Nhà phát triển phần mềm viết ứng dụng bằng Java, Ruby, Python có thư viện tinh vi giúp duy trì tính toàn vẹn phần mềm theo thời gian. Họ tạo bài kiểm tra chạy ứng dụng qua chuỗi thực thi trong môi trường có cấu trúc để đảm bảo mọi khía cạnh phần mềm hoạt động như mong đợi.
Các bài kiểm tra này còn mạnh mẽ hơn khi được tự động hóa trong hệ thống tích hợp liên tục (CI), nơi mọi push lên kho nguồn khiến test chạy, và lập trình viên được thông báo ngay khi test thất bại. Phản hồi nhanh này tăng niềm tin của lập trình viên vào tính toàn vẹn chức năng của ứng dụng.
Hệ thống Kiểm Thử Tự Động Bash (BATS) cho phép nhà phát triển viết script và thư viện Bash áp dụng cùng thực hành như nhà phát triển Java, Ruby, Python vào code Bash của họ.
Cài đặt BATS
Trang GitHub BATS bao gồm hướng dẫn cài đặt. Có hai thư viện hỗ trợ BATS cung cấp assertion mạnh mẽ hơn hoặc cho phép ghi đè định dạng đầu ra Test Anything Protocol (TAP) dùng bởi BATS. Chúng có thể cài ở vị trí chuẩn và nạp bởi tất cả script. Có thể tiện lợi hơn là bao gồm phiên bản hoàn chỉnh BATS và thư viện hỗ trợ trong kho Git cho từng tập script/thư viện được test. Điều này có thể thực hiện dùng hệ thống git submodule.
Các lệnh sau sẽ cài BATS và thư viện hỗ trợ vào thư mục test trong kho Git.
git submodule init
git submodule add https://github.com/sstephenson/bats test/libs/bats
git submodule add https://github.com/ztombol/bats-assert test/libs/bats-assert
git submodule add https://github.com/ztombol/bats-support test/libs/bats-support
git add .
git commit -m 'installed bats'
Để clone kho Git và cài submodule cùng lúc, dùng cờ --recurse-submodules cho git clone.
Mỗi script test BATS phải được thực thi bởi file thực thi bats. Nếu bạn cài BATS vào thư mục test/libs của kho nguồn, bạn có thể gọi test bằng:
./test/libs/bats/bin/bats <đường dẫn script test>
Thay vào đó, thêm sau vào đầu mỗi script test BATS:
#!/usr/bin/env ./test/libs/bats/bin/bats
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
và chmod +x <đường dẫn script test>. Điều này sẽ a) làm cho chúng có thể thực thi với BATS cài trong ./test/libs/bats và b) bao gồm các thư viện hỗ trợ. Script test BATS thường lưu trong thư mục test và đặt tên theo script được test, nhưng với phần mở rộng .bats. Ví dụ, script BATS test bin/build nên gọi là test/build.bats.
Bạn cũng có thể chạy toàn bộ tập file test BATS bằng cách truyền biểu thức chính quy cho BATS, ví dụ ./test/lib/bats/bin/bats test/*.bats.
Tổ chức thư viện và script để BATS bao phủ
Script và thư viện Bash phải được tổ chức để hiệu quả phơi bày hoạt động bên trong cho BATS. Nói chung, hàm thư viện và shell script chạy nhiều lệnh khi được gọi hoặc thực thi không thuận lợi cho test BATS hiệu quả.
Ví dụ, build.sh là script điển hình nhiều người viết. Nó về cơ bản là một đống code lớn. Một số thậm chí đặt đống code đó trong hàm trong thư viện. Nhưng không thể chạy đống code lớn trong test BATS và bao phủ mọi loại thất bại có thể gặp trong các trường hợp test riêng biệt. Cách duy nhất để test đống code đó với bao phủ đủ là chia nhỏ thành nhiều hàm nhỏ, tái sử dụng được, và quan trọng nhất, có thể test độc lập.
Việc thêm hàm vào thư viện rất đơn giản. Lợi ích thêm là một số hàm đó có thể trở nên hữu ích bất ngờ. Khi bạn đã chia hàm thư viện thành nhiều hàm nhỏ, bạn có thể source thư viện trong test BATS và chạy hàm như bất kỳ lệnh nào khác để test chúng.
Script Bash cũng phải chia nhỏ thành nhiều hàm, phần chính của script sẽ gọi khi script được thực thi. Ngoài ra, có một thủ thuật rất hữu ích để test script Bash với BATS dễ dàng hơn: Lấy toàn bộ code được thực thi trong phần chính của script và chuyển vào hàm, gọi như run_main. Sau đó, thêm sau vào cuối script:
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
run_main
fi
Đoạn code thêm này làm điều đặc biệt. Nó làm cho script hành xử khác khi được thực thi như script so với khi được đưa vào môi trường bằng source. Thủ thuật này cho phép script được test giống như thư viện, bằng cách source nó và test từng hàm riêng lẻ. Ví dụ, đây là build.sh đã refactor để test BATS tốt hơn.
Viết và chạy test
Như đã nói, BATS là khung kiểm tra tuân thủ TAP với cú pháp và đầu ra quen thuộc với người dùng các bộ test tuân thủ TAP khác, như JUnit, RSpec, hoặc Jest. Test được tổ chức thành các script test riêng lẻ. Script test được tổ chức thành một hoặc nhiều khối @test mô tả đơn vị ứng dụng được test. Mỗi khối @test sẽ chạy chuỗi lệnh chuẩn bị môi trường test, chạy lệnh được test, và đưa ra assertion về mã thoát và đầu ra của lệnh test. Nhiều hàm assertion được nhập với thư viện bats, bats-assert, và bats-support, được nạp vào môi trường ở đầu script test BATS. Đây là khối test BATS điển hình:
@test "requires CI_COMMIT_REF_SLUG environment variable" {
unset CI_COMMIT_REF_SLUG
assert_empty "${CI_COMMIT_REF_SLUG}"
run some_command
assert_failure
assert_output --partial "CI_COMMIT_REF_SLUG"
}
Nếu script BATS bao gồm hàm setup và/hoặc teardown, chúng được BATS tự động thực thi trước và sau mỗi khối test chạy. Điều này cho phép tạo biến môi trường, file test, và làm những việc khác cần thiết cho một hoặc tất cả test, rồi dọn dẹp sau mỗi test chạy. Build.bats là một test BATS đầy đủ của build.sh mới định dạng. (Lệnh mock_docker trong test này sẽ được giải thích bên dưới, trong phần mocking/stubbing.)
Khi script test chạy, BATS dùng exec chạy mỗi khối @test như một tiến trình con riêng biệt. Điều này cho phép xuất biến môi trường và thậm chí hàm trong một @test mà không ảnh hưởng @test khác hoặc làm ô nhiễm phiên shell hiện tại. Đầu ra của lần chạy test là định dạng chuẩn có thể hiểu bởi con người và phân tích hoặc thao tác lập trình bởi consumer TAP. Đây là ví dụ đầu ra cho khối test CI_COMMIT_REF_SLUG khi nó thất bại:
✗ requires CI_COMMIT_REF_SLUG environment variable
(from function `assert_output' in file test/libs/bats-assert/src/assert.bash, line 231,
in test file test/ci_deploy.bats, line 26)
`assert_output --partial "CI_COMMIT_REF_SLUG"' failed
-- output does not contain substring --
substring (1 lines):
CI_COMMIT_REF_SLUG
output (3 lines):
./bin/deploy.sh: join_string_by: command not found
oc error
Could not login
--
** Did not delete , as test failed **
1 test, 1 failure
Đây là đầu ra của test thành công:
✓ requires CI_COMMIT_REF_SLUG environment variable
Trợ giúp (Helpers)
Giống như bất kỳ script hoặc thư viện shell nào, script test BATS có thể bao gồm thư viện trợ giúp để chia sẻ code chung giữa các test hoặc tăng cường khả năng. Các thư viện trợ giúp này, như bats-assert và bats-support, thậm chí có thể được test bằng BATS.
Thư viện có thể đặt trong cùng thư mục test với script BATS hoặc trong thư mục test/libs nếu số file trong thư mục test trở nên khó kiểm soát. BATS cung cấp hàm load nhận đường dẫn đến file Bash tương đối với script được test (ví dụ test, trong trường hợp chúng ta) và source file đó. File phải kết thúc bằng tiền tố .bash, nhưng đường dẫn truyền cho hàm load không thể bao gồm tiền tố. build.bats nạp bats-assert và bats-support, thư viện helpers.bash nhỏ, và thư viện docker_mock.bash (mô tả bên dưới) với mã sau đặt ở đầu script test dưới dòng interpreter magic:
load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'helpers'
load 'docker_mock'
Stub đầu vào test và mock gọi bên ngoài
Đa số script và thư viện Bash thực thi hàm và/hoặc file thực thi khi chạy. Thường chúng được lập trình hành xử theo cách đặc định dựa trên mã thoát hoặc đầu ra (stdout, stderr) của hàm hoặc file thực thi đó. Để test đúng các script này, thường cần tạo phiên bản giả của các lệnh đó được thiết kế hành xử theo cách đặc biệt trong test đặc biệt, quá trình gọi là "stubbing". Có thể cũng cần gián điệp chương trình được test để đảm bảo nó gọi lệnh đặc định, hoặc gọi lệnh đó với đối số đặc định, quá trình gọi là "mocking". Để biết thêm, xem thảo luận tuyệt vời về mocking và stubbing trong Ruby RSpec, áp dụng cho mọi hệ thống test.
Shell Bash cung cấp thủ thuật có thể dùng trong script test BATS để làm mocking và stubbing. Tất cả yêu cầu dùng lệnh Bash export với cờ -f để xuất hàm ghi đè hàm gốc hoặc file thực thi. Điều này phải làm trước khi chương trình được test thực thi. Đây là ví dụ đơn giản ghi đè file thực thi cat:
function cat() { echo "THIS WOULD CAT ${*}" }
export -f cat
Phương pháp này ghi đè hàm theo cách tương tự. Nếu test cần ghi đè hàm trong script hoặc thư viện được test, quan trọng là source script hoặc thư viện được test trước khi hàm bị stub hoặc mock. Nếu không, stub/mock sẽ bị thay thế bằng hàm thực khi script được source. Cũng nhớ stub/mock trước khi chạy lệnh bạn test. Đây là ví dụ từ build.bats mock hàm raise mô tả trong build.sh để đảm bảo thông báo lỗi cụ thể được raise bởi hàm login:
@test ".login raises on oc error" {
source ${profile_script}
function raise() { echo "${1} raised"; }
export -f raise
run login
assert_failure
assert_output -p "Could not login raised"
}
Thông thường, không cần unset hàm stub/mock sau test, vì export chỉ ảnh hưởng tiến trình con hiện tại trong exec của khối @test hiện tại. Tuy nhiên, có thể mock/stub lệnh (ví dụ cat, sed, v.v.) mà hàm assert* của BATS dùng nội bộ. Các hàm mock/stub này phải unset trước khi chạy lệnh assertion, hoặc chúng sẽ không hoạt động đúng. Đây là ví dụ từ build.bats mock sed, chạy hàm build_deployable, và unset sed trước khi chạy assertion:
@test ".build_deployable prints information, runs docker build on a modified Dockerfile.production and publish_image when its not a dry_run" {
local expected_dockerfile='Dockerfile.production'
local application='application'
local environment='environment'
local expected_original_base_image="${application}"
local expected_candidate_image="${application}-candidate:${environment}"
local expected_deployable_image="${application}:${environment}"
source ${profile_script}
mock_docker build --build-arg OAUTH_CLIENT_ID --build-arg OAUTH_REDIRECT --build-arg DDS_API_BASE_URL -t "${expected_deployable_image}" -
function publish_image() { echo "publish_image ${*}"; }
export -f publish_image
function sed() {
echo "sed ${*}" >&2;
echo "FROM application-candidate:environment";
}
export -f sed
run build_deployable "${application}" "${environment}"
assert_success
unset sed
assert_output --regexp "sed.*${expected_dockerfile}"
assert_output -p "Building ${expected_original_base_image} deployable ${expected_deployable_image} FROM ${expected_candidate_image}"
assert_output -p "FROM ${expected_candidate_image} piped"
assert_output -p "build --build-arg OAUTH_CLIENT_ID --build-arg OAUTH_REDIRECT --build-arg DDS_API_BASE_URL -t ${expected_deployable_image} -"
assert_output -p "publish_image ${expected_deployable_image}"
}
Đôi khi cùng một lệnh, ví dụ foo, sẽ được gọi nhiều lần, với đối số khác nhau, trong cùng hàm được test. Những tình huống này yêu cầu tạo một tập hàm:
- mock_foo: nhận đối số kỳ vọng làm đầu vào, và lưu chúng vào file TMP
- foo: phiên bản mock của lệnh, xử lý mỗi lần gọi với danh sách đối số kỳ vọng đã lưu. Phải xuất với export -f.
- cleanup_foo: xóa file TMP, dùng trong hàm teardown. Có thể test đảm bảo khối @test thành công trước khi xóa.
Vì chức năng này thường tái sử dụng trong test khác, nên tạo thư viện trợ giúp có thể nạp như thư viện khác là hợp lý.
Ví dụ tốt là docker_mock.bash. Nó được nạp vào build.bats và dùng trong bất kỳ khối test nào test hàm gọi file thực thi Docker. Khối test điển hình dùng docker_mock trông như:
@test ".publish_image fails if docker push fails" {
setup_publish
local expected_image="image"
local expected_publishable_image="${CI_REGISTRY_IMAGE}/${expected_image}"
source ${profile_script}
mock_docker tag "${expected_image}" "${expected_publishable_image}"
mock_docker push "${expected_publishable_image}" and_fail
run publish_image "${expected_image}"
assert_failure
assert_output -p "tagging ${expected_image} as ${expected_publishable_image}"
assert_output -p "tag ${expected_image} ${expected_publishable_image}"
assert_output -p "pushing image to gitlab registry"
assert_output -p "push ${expected_publishable_image}"
}
Test này thiết lập kỳ vọng Docker sẽ được gọi hai lần với đối số khác nhau. Với lần gọi thứ hai thất bại, nó chạy lệnh được test, rồi test mã thoát và các lần gọi Docker kỳ vọng.
Một khía cạnh BATS do mock_docker.bash giới thiệu là biến môi trường ${BATS_TMPDIR}, BATS thiết lập ở đầu để cho phép test và helper tạo và hủy file TMP ở vị trí chuẩn. Thư viện mock_docker.bash sẽ không xóa file mock đã lưu nếu test thất bại, nhưng sẽ in vị trí nó để có thể xem và xóa. Bạn có thể cần dọn dẹp file mock cũ khỏi thư mục này định kỳ.
Một lưu ý về mocking/stubbing: Test build.bats có ý thức vi phạm một nguyên tắc test: Đừng mock những gì bạn không sở hữu! Nguyên tắc này yêu cầu gọi lệnh mà nhà phát triển test không tự viết, như docker, cat, sed, v.v., nên được bọc trong thư viện riêng, và thư viện đó nên được mock trong test của script dùng chúng. Thư viện bọc sau đó nên được test mà không mock lệnh bên ngoài.
Đây là lời khuyên tốt và bỏ qua nó có chi phí. Nếu CLI API Docker thay đổi, script test sẽ không phát hiện thay đổi này, dẫn đến false positive chỉ biểu hiện khi build.sh test chạy trong production với phiên bản Docker mới. Nhà phát triển test phải quyết định họ muốn tuân thủ tiêu chuẩn này đến mức nào, nhưng họ nên hiểu sự đánh đổi liên quan đến quyết định của họ.
Kết luận
Giới thiệu chế độ test vào bất kỳ dự án phát triển phần mềm nào tạo ra sự đánh đổi giữa a) tăng thời gian và tổ chức cần để phát triển và duy trì code và test và b) tăng niềm tin lập trình viên vào tính toàn vẹn ứng dụng suốt vòng đời. Chế độ test có thể không phù hợp cho tất cả script và thư viện.
Nói chung, script và thư viện đáp ứng một hoặc nhiều điều sau nên test với BATS:
- Chúng đáng để lưu trong kiểm soát nguồn
- Chúng dùng trong quy trình quan trọng và được頼以 chạy nhất quán trong thời gian dài
- Chúng cần sửa đổi định kỳ để thêm/bỏ/sửa chức năng
- Chúng được người khác sử dụng
Khi quyết định áp dụng kỷ luật test cho một hoặc nhiều script/thư viện Bash, BATS cung cấp tính năng test toàn diện có sẵn trong môi trường phát triển phần mềm khác.
Lời cảm ơn: Tôi biết ơn Darrin Mann vì đã giới thiệu tôi về test BATS.