Cài Đặt Hữu Ích Khi Chạy RSpec Với parallel_tests
Bài viết này sẽ mô tả một số cài đặt hữu ích khi chạy kiểm thử với parallel_tests, đặc biệt là cho việc tái hiện và điều tra các lỗi xảy ra trong các lần chạy CI.
Giới Thiệu về parallel_tests
Trong môi trường CI, chúng ta thường chạy RSpec song song để tăng tốc độ kiểm thử. Để làm điều này, có một gem rất tiện lợi gọi là parallel_tests. Gem này giúp tận dụng hiệu quả các CPU đa nhân.
Mặc dù tài liệu hướng dẫn cài đặt đã đủ chi tiết, nhưng việc gỡ lỗi trở nên khó khăn hơn khi việc thực thi song song tạo ra các bài kiểm thử không ổn định (flaky tests).
Trong bài viết này, tôi sẽ chia sẻ một số MẸO để xử lý các bài kiểm thử không ổn định khi chạy RSpec với parallel_tests trong CI.
MẸO
1. Cố định hạt giống (seed) trên tất cả các tiến trình
Việc sử dụng hạt giống (seed) của RSpec rất hữu ích để tái hiện lỗi với cùng một thứ tự kiểm thử. Khi chạy kiểm thử song song, mỗi tiến trình sẽ nhận một giá trị hạt giống riêng. Tuy nhiên, để gỡ lỗi, hiệu quả hơn nếu tất cả các tiến trình chia sẻ cùng một hạt giống.
Điều này không có nghĩa là luôn chạy với một hạt giống cố định — mà là bạn tạo ra một số ngẫu nhiên một lần và truyền nó rõ ràng cho tất cả các tiến trình.
Nếu không có --seed, RSpec sẽ tự động quyết định hạt giống nội bộ tại đây:
Hạt giống ngẫu nhiên của RSpec
RSpec sử dụng rand(0xFFFF). Vì vậy, bằng cách truyền cùng một giá trị thông qua --seed, bạn có thể đồng bộ hóa tất cả các tiến trình.
Ví dụ:
ruby
$ ruby -e "puts rand(0xffff)"
22357
$ ruby -e "puts rand(0xffff)"
7574
$ ruby -e "puts rand(0xffff)"
11717
Sau đó, chạy kiểm thử của bạn như sau:
ruby
$ bundle exec parallel_rspec -- --seed $(ruby -e "puts rand(0xFFFF)") -- spec/
Để biết thêm về việc sử dụng --seed để xử lý các bài kiểm thử không ổn định trong RSpec, tôi đã viết một bài viết khác:
Kiểm soát các bài kiểm thử không ổn định của bạn bằng cách cố định hạt giống
2. Xem tệp nào mỗi tiến trình chịu trách nhiệm
Khi bạn chạy parallel_rspec, các tệp kiểm thử sẽ được phân phối qua các tiến trình. Tuy nhiên, theo mặc định, bạn không thể biết tiến trình nào đang xử lý tệp nào từ đầu ra.
Để làm cho điều này trở nên rõ ràng hơn, bạn có thể sử dụng hook before(:suite) của RSpec:
ruby
# spec/rails_helper.rb
RSpec.configure do |config|
config.before(:suite) do
files = config.files_to_run
normalized = files.map { Pathname.new(File.absolute_path(it)).relative_path_from(Rails.root) }
banner = "PID (#{Process.pid}) #{normalized.count} tệp để chạy:"
puts [banner, *normalized].join("\n\t")
# ...
end
# ...
end
Giờ đây, mỗi tiến trình sẽ xuất ra một cái gì đó như sau:
ruby
PID (1075695) 5 tệp để chạy:
spec/controllers/bars_controller_spec.rb
spec/controllers/foos_controller_spec.rb
spec/models/bar_spec.rb
spec/models/baz_spec.rb
spec/models/foo_spec.rb
3. Xem thứ tự thực thi của các tệp trong mỗi tiến trình
Đôi khi, các lỗi phụ thuộc vào thứ tự thực thi của các tệp kiểm thử. Với các MẸO trước, bạn biết hạt giống nào và bộ tệp nào đã được gán cho một tiến trình, nhưng không biết thứ tự thực thi thực tế.
Giả sử CI hiển thị lỗi này:
ruby
PID (1075695) 5 tệp để chạy:
spec/controllers/bars_controller_spec.rb
spec/controllers/foos_controller_spec.rb
spec/models/bar_spec.rb
spec/models/baz_spec.rb
spec/models/foo_spec.rb
ruby
Ngẫu nhiên với hạt giống 54242
Bạn có thể tái hiện nó tại địa phương như sau:
ruby
bundle exec rspec --seed 54242 \
spec/controllers/bars_controller_spec.rb \
spec/controllers/foos_controller_spec.rb \
spec/models/bar_spec.rb \
spec/models/baz_spec.rb \
spec/models/foo_spec.rb
Tất nhiên, điều này hoạt động tốt, nhưng nếu nhật ký thực thi CI trông như sau?
(Tôi đang sử dụng định dạng tiến trình)
ruby
...F...........
Bằng cách nào đó, bạn có thể đoán rằng spec/models/foo_spec.rb đã được thực thi tương đối sớm. Ngay cả khi bạn biết tệp này thực sự là tệp thứ hai được chạy, tại thời điểm này bạn vẫn cần bao gồm tất cả năm tệp để tái hiện lỗi.
(Và tất nhiên, luôn có cách “gian lận” bằng cách dựa vào trực giác của một lập trình viên có kinh nghiệm…)
Đó là lý do tại sao, nếu bạn có thể thấy thứ tự thực thi thực tế của các tệp kiểm thử, bạn có thể đơn giản hóa các bước tái hiện. Hãy cập nhật mã trước đó như sau:
ruby
RSpec.configure do |config|
config.before(:suite) do
- files = config.files_to_run
+ files = config.world.ordered_example_groups.map { it.file_path }
normalized = files.map { Pathname.new(File.absolute_path(it)).relative_path_from(Rails.root) }
banner = "PID (#{Process.pid}) #{normalized.count} tệp để chạy:"
puts [banner, *normalized].join("\n\t")
# ...
end
# ...
end
Sau khi thực hiện các điều chỉnh trên, đầu ra sẽ xuất hiện như sau. Thứ tự của các tệp đã thay đổi, phải không?
ruby
PID (1075695) 5 tệp để chạy:
spec/controllers/foos_controller_spec.rb
spec/models/foo_spec.rb
spec/models/bar_spec.rb
spec/models/baz_spec.rb
spec/controllers/foos_controller_spec.rb
Sau đó, RSpec sẽ tiếp tục với các bài kiểm thử theo thứ tự mà chúng được xuất ra ở đây. Bên cạnh đó, vì bài kiểm thử bị lỗi nằm trong spec/models/foo_spec.rb, bạn không cần phải chạy bất kỳ tệp nào sau đó để tái hiện vấn đề. Điều này cho thấy rằng các bước tái hiện sau đây là đủ. Các bước tái hiện đã được đơn giản hóa đáng kể! Điều này tiết kiệm rất nhiều thời gian.
ruby
bundle exec rspec --seed 54242 \
spec/controllers/foos_controller_spec.rb \
spec/models/foo_spec.rb
⚠️ Lưu ý: Cả world và ordered_example_groups đều là API riêng tư:
Vì vậy, hãy lưu ý rằng chúng có thể thay đổi mà không cần thông báo trước.
Kết Luận
parallel_tests là công cụ tuyệt vời để tăng tốc CI, nhưng sự song song của nó thường làm cho việc gỡ lỗi các bài kiểm thử không ổn định trở nên khó khăn hơn. Với các MẸO được nêu ở trên, việc tái hiện và điều tra các lỗi như vậy trở nên dễ dàng hơn rất nhiều.
Ngay cả khi kết quả kiểm thử của bạn trông lộn xộn lúc đầu, với thiết lập đúng, bạn có thể lần theo nguyên nhân gốc rễ một cách có hệ thống — và giữ cho đường ống CI của bạn ổn định.