اختبار الدوال بوحدة باستخدام Bats-core
نظّم ملفات الاختبار والتأكيدات وإعداد الاختبارات وإنهائها للتحقّق من دوال Bash الفردية
اختبار الدوال بوحدة باستخدام Bats-core درس مجاني في DevOps Bootcamp على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DevOps Bootcamp، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.
ما هو Bats-core ولماذا نستخدمه؟
Bats-core (Bash Automated Testing System) هو إطار العمل الفعلي لاختبار الوحدات في Bash. يتيح لكم كتابة اختبارات منظّمة وقابلة للتكرار لدوال وبرامج shell، بالطريقة نفسها التي تستخدمون بها JUnit مع Java أو pytest مع Python.
- كل اختبار عبارة عن كتلة
@testتتضمن وصفًا واضحًا باللغة الطبيعية. - ينجح الاختبار عندما يعيد كل أمر بداخله رمز الخروج
0. - يفشل الاختبار عند أول رمز خروج غير صفري أو عند فشل تأكيد.
- يكون الإخراج متوافقًا مع TAP، ولذلك تتعرّف عليه أنظمة CI مثل GitHub Actions وJenkins وGitLab CI مباشرةً.
ثبّتوه باستخدام مدير الحزم لديكم أو استنسخوا المستودع:
# Install via git (recommended — always latest)
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local
# Or on macOS with Homebrew
brew install bats-core
# Verify installation
bats --version
# bats 1.x.yملف اختبار Bats الأول
يمتلك ملف اختبار Bats الامتداد .bats ويبدأ بـ shebang خاص. أما لبنة البناء الأساسية فهي التوجيه @test متبوعًا بسلسلة وصفية وكتلة من الأوامر.
- يُخبر shebang
#!/usr/bin/env batsshell بكيفية تنفيذ الملف. - كل كتلة
@testهي حالة اختبار مستقلة. - يمكنكم تشغيل ملف واحد باستخدام
bats my_tests.batsأو مجلد كامل باستخدامbats test/.
فيما يلي البنية الأساسية لملف اختبار Bats:
#!/usr/bin/env bats
# File: test/hello.bats
@test "echo outputs the expected string" {
result=$(echo "hello world")
[ "$result" = "hello world" ]
}
@test "false command causes test to fail" {
# Uncommenting the next line would make this test fail:
# false
true
}تحميل الدالة قيد الاختبار باستخدام 'load'
في المشاريع الفعلية، توجد دوال Bash في ملفات مكتبات، وليس داخل ملف الاختبار نفسه. يوفّر Bats المساعد load لتحميل ملفات خارجية مصدريةً بالنسبة إلى مجلد ملف الاختبار.
- يحمّل
load '../lib/math.sh'الملف قبل تشغيل كل اختبار. - بعد التحميل، تصبح كل الدوال المعرّفة في ذلك الملف متاحة داخل كتل الاختبار لديكم.
- احرصوا على وضع دوال المكتبة في مجلد
lib/والاختبارات في مجلدtest/للفصل الواضح بينهما.
مثال على تخطيط المشروع والاختبار المقابل:
# Project layout:
# lib/math.sh <- functions to test
# test/math.bats <- test file
# lib/math.sh
add() {
echo $(( $1 + $2 ))
}
divide() {
if [ "$2" -eq 0 ]; then
echo "Error: division by zero" >&2
return 1
fi
echo $(( $1 / $2 ))
}
# test/math.bats
#!/usr/bin/env bats
load '../lib/math.sh'
@test "add returns correct sum" {
result=$(add 3 4)
[ "$result" = "7" ]
}التأكيدات الأساسية: run و$status و$output
يُعد الأمر run جوهر الاختبار باستخدام Bats. فبدلًا من تنفيذ الأمر مباشرةً، يؤدي تغليفه بـ run إلى التقاط رمز الخروج والإخراج الخاصين به من دون التسبب في فشل الاختبار فورًا.
$status— يحتوي على رمز خروج آخر أمرrun.$output— يحتوي على stdout المدمج لآخر أمرrun.$lines— مصفوفة يكون كل عنصر فيها سطرًا واحدًا من الإخراج (${lines[0]}و${lines[1]}وغيرهما).
يتيح لكم ذلك إجراء تأكيدات على حالات النجاح والفشل معًا:
#!/usr/bin/env bats
load '../lib/math.sh'
@test "divide 10 by 2 returns 5" {
run divide 10 2
[ "$status" -eq 0 ]
[ "$output" = "5" ]
}
@test "divide by zero returns exit code 1" {
run divide 10 0
[ "$status" -eq 1 ]
}
@test "divide by zero prints error message" {
run divide 10 0
# $output captures stderr too when redirected inside the function
[[ "$output" == *"division by zero"* ]]
}استخدام bats-assert لإجراء تأكيدات واضحة
تعمل تأكيدات [ ] المضمّنة، لكنها تعطي رسائل فشل غير مفيدة. توفّر مكتبة المساعد bats-assert دوال تأكيد واضحة تطبع الخطأ الذي حدث تحديدًا.
assert_success— تؤكد أن قيمة$statusتساوي 0.assert_failure— تؤكد أن قيمة$statusغير صفرية.assert_output— تؤكد أن قيمة$outputتساوي السلسلة المحددة.assert_output --partial— تؤكد أن الإخراج يحتوي على السلسلة الفرعية.refute_output --partial— تؤكد أن الإخراج لا يحتوي على السلسلة الفرعية.
ثبّتوا المكتبة باستنساخ bats-core/bats-assert داخل مجلد test/helpers/، ثم حمّلوها:
#!/usr/bin/env bats
# Load bats-assert (cloned into test/helpers/bats-assert)
load 'helpers/bats-assert/load'
load '../lib/math.sh'
@test "add 5 and 3 gives 8" {
run add 5 3
assert_success
assert_output "8"
}
@test "divide by zero fails with descriptive message" {
run divide 9 0
assert_failure
assert_output --partial "division by zero"
}
@test "add does not output an error" {
run add 1 1
refute_output --partial "Error"
}setup وteardown: خطافات دورة حياة الاختبار
يوفّر Bats دالتين خاصتين — setup وteardown — تُشغّلان تلقائيًا قبل كل اختبار وبعده. استخدموهما لإعداد الحالة المشتركة وتنظيفها، حتى يبدأ كل اختبار في بيئة معروفة.
- تُشغّل
setup()قبل كل كتلة@testعلى حدة. - تُشغّل
teardown()بعد كل كتلة@testعلى حدة، حتى إذا فشل الاختبار. - من الاستخدامات الشائعة: إنشاء مجلدات مؤقتة، وضبط متغيرات البيئة، وإزالة الملفات المؤقتة بعد الاختبار.
#!/usr/bin/env bats
load '../lib/fileutils.sh'
setup() {
# Create a fresh temp directory before every test
TEST_DIR=$(mktemp -d)
export TEST_DIR
}
teardown() {
# Always clean up, even on test failure
rm -rf "$TEST_DIR"
}
@test "write_file creates a file with correct content" {
run write_file "$TEST_DIR/hello.txt" "hello world"
assert_success
[ -f "$TEST_DIR/hello.txt" ]
[ "$(cat "$TEST_DIR/hello.txt")" = "hello world" ]
}
@test "write_file fails when directory does not exist" {
run write_file "/nonexistent/dir/file.txt" "data"
assert_failure
}setup_file وteardown_file: خطافات على مستوى مجموعة الاختبارات
أحيانًا تحتاجون إلى إعداد موارد مكلفة مرة واحدة لكل ملف، وليس قبل كل اختبار. يوفّر Bats setup_file وteardown_file لهذا الغرض.
- تُشغّل
setup_file()مرة واحدة قبل جميع الاختبارات في الملف. - تُشغّل
teardown_file()مرة واحدة بعد جميع الاختبارات في الملف. - استخدموا
BATS_FILE_TMPDIR(المتاح تلقائيًا) لمشاركة البيانات بينsetup_fileوالاختبارات؛ إذ لا تستمر المتغيرات العادية عبر subshells.
حالة الاستخدام المعتادة هي تشغيل خادم وهمي أو بناء ملف ثنائي مرة واحدة، ثم إيقافه في النهاية:
#!/usr/bin/env bats
setup_file() {
# Build the project binary once for all tests in this file
make build --silent
export BINARY="$PWD/bin/myapp"
echo "Binary built: $BINARY"
}
teardown_file() {
# Remove the binary after all tests complete
rm -f "$BINARY"
echo "Cleaned up binary"
}
setup() {
# Still runs before each individual test
TEST_TMP=$(mktemp -d)
}
teardown() {
rm -rf "$TEST_TMP"
}
@test "myapp --version outputs version string" {
run "$BINARY" --version
assert_output --partial "1.0"
}اختبار الدوال التي تعدّل الملفات
من الأنماط الشائعة جدًا اختبار دوال Bash التي تقرأ من نظام الملفات أو تكتب فيه. تتمثل التقنية الأساسية في استخدام مجلدات مؤقتة (عبر mktemp -d داخل setup) حتى لا تلمس الاختبارات الملفات الفعلية ولا تتداخل مع بعضها.
- اعملوا دائمًا داخل
$TEST_DIR(أو$BATS_TEST_TMPDIR— المتاح تلقائيًا في إصدارات Bats الحديثة). - استخدموا مكتبة المساعد
bats-fileلإجراء تأكيدات واضحة على الملفات، مثلassert_file_existsوassert_file_contains. - لا تضعوا مسارات ثابتة مثل
/tmp/myfile؛ إذ ستتعارض عمليات تشغيل الاختبارات بالتوازي.
#!/usr/bin/env bats
load 'helpers/bats-assert/load'
load 'helpers/bats-file/load'
load '../lib/fileutils.sh'
setup() {
TEST_DIR="$BATS_TEST_TMPDIR"
}
# lib/fileutils.sh defines:
# append_line() { echo "$2" >> "$1"; }
@test "append_line adds a line to an existing file" {
echo "first line" > "$TEST_DIR/log.txt"
run append_line "$TEST_DIR/log.txt" "second line"
assert_success
assert_file_contains "$TEST_DIR/log.txt" "second line"
}
@test "append_line creates file if it does not exist" {
run append_line "$TEST_DIR/new.txt" "hello"
assert_success
assert_file_exists "$TEST_DIR/new.txt"
}محاكاة الأوامر الخارجية
غالبًا ما تستدعي الدوال برامج خارجية مثل curl أو aws أو git. في اختبارات الوحدات، تريدون اختبار منطقكم أنتم، لا الأمر الخارجي الفعلي. وتتمثل أنظف تقنية للمحاكاة في Bats في تعريف دالة shell بالاسم نفسه للأمر داخل setup؛ إذ تكون لها الأولوية على الملف الثنائي الفعلي.
- عرّفوا دالة مثل
curl() { echo 'mocked response'; return 0; }داخلsetupوصدّروها. - استخدموا
export -f curlحتى تكون الدالة مرئية في subshells التي ينشئهاrun. - يمكنكم أيضًا كتابة المحاكاة في ملف مؤقت موجود على
PATHللسيناريوهات الأكثر تعقيدًا.
#!/usr/bin/env bats
load 'helpers/bats-assert/load'
load '../lib/network.sh'
# lib/network.sh defines:
# fetch_status() {
# local url="$1"
# local code
# code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
# echo "$code"
# }
setup() {
# Override 'curl' with a mock function
curl() {
# Simulate a 200 OK response
echo "200"
return 0
}
export -f curl
}
@test "fetch_status returns 200 when curl reports 200" {
run fetch_status "https://example.com"
assert_success
assert_output "200"
}تخطي الاختبارات ووضع الوسوم
لا يمكن دائمًا تشغيل كل اختبار؛ فقد تحتاجون أحيانًا إلى اتصال حقيقي بالشبكة أو أداة محددة أو نظام تشغيل معيّن. يوفّر Bats الأمر skip لتجاوز اختبار بشكل مشروط مع عرض رسالة توضّح السبب، بدلًا من تعليق الاختبار أو تعطيل مجموعة الاختبارات.
- استدعوا
skip "reason"في أي موضع داخل كتلة@testلتخطي ذلك الاختبار. - تظهر الاختبارات المتخطاة في الإخراج بالحرف
Sولا تُحتسب كاختبارات فاشلة. - يدعم Bats 1.5 والإصدارات الأحدث الوسوم: أضيفوا وسمًا إلى الاختبارات باستخدام
# bats test_tags=slow,network، ثم صفّوا النتائج باستخدامbats --filter-tags network test/.
#!/usr/bin/env bats
# bats test_tags=network
@test "API returns valid JSON" {
# Skip if no internet connectivity
if ! ping -c1 -W1 8.8.8.8 &>/dev/null; then
skip "No network connection available"
fi
run curl -s "https://api.example.com/health"
assert_success
assert_output --partial '"status"'
}
# bats test_tags=unit
@test "slug function lowercases and replaces spaces" {
# Always runs — pure function, no external deps
slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-'; }
run slug "Hello World"
assert_output "hello-world"
}
# Run only unit tests:
# bats --filter-tags unit test/تنظيم مجموعة اختبارات متكاملة
يتبع مشروع Bats المنظّم جيدًا تخطيطًا متوقعًا للمجلدات، مما يسهّل ضم المساهمين الجدد ودمجه مع مسارات CI.
البنية الموصى بها:
lib/— دوال Bash الخاصة بالإنتاج، بمعدل ملف واحد لكل مجال:math.shوfileutils.sh.test/— ملف.batsواحد لكل ملف مكتبة:math.batsوfileutils.bats.test/helpers/— bats-assert وbats-file وbats-support بوصفها وحدات فرعية في git.Makefile— هدفtestحتى يتمكن المساهمون من تشغيلmake testفقط.
شغّلوا مجموعة الاختبارات كاملةً بأمر واحد:
# Makefile
.PHONY: test
test:
bats test/
# Run all tests recursively (Bats 1.5+)
# bats --recursive test/
# Run a specific file
# bats test/math.bats
# Run with verbose (TAP) output for CI
# bats --tap test/
# Example directory tree:
# .
# |-- lib/
# | |-- math.sh
# | `-- fileutils.sh
# |-- test/
# | |-- helpers/
# | | |-- bats-assert/
# | | `-- bats-file/
# | |-- math.bats
# | `-- fileutils.bats
# `-- Makefileاختبروا معارفكم: تأكيدات Bats-core
اختبروا مدى فهمكم لآلية الاختبار الأساسية في Bats-core.
مراجعة: اختبار وحدات Bash باستخدام Bats-core
تعلّمتم في هذا الدرس كيفية تنظيم اختبارات الوحدات وكتابتها لدوال Bash الفردية باستخدام Bats-core. فيما يلي أهم النقاط:
- بنية ملف الاختبار — استخدموا shebang
#!/usr/bin/env batsوكتل@testذات أسماء وصفية. load— حمّلوا ملفات المكتبة حتى تصبح الدوال متاحة في الاختبارات من دون نسخها ولصقها.run+$status+$output— الثلاثي الأساسي؛ استخدمواrunدائمًا لالتقاط النتائج من دون التسبب في فشل الاختبار فورًا.- bats-assert — فضّلوا
assert_successوassert_failureوassert_outputعلى[ ]الخام للحصول على رسائل فشل سهلة القراءة. setup/teardown— تُشغّلان قبل كل اختبار وبعده؛ أماsetup_file/teardown_fileفتُشغّلان مرة واحدة لكل ملف.- المحاكاة — احجبوا الأوامر الخارجية بدوال shell تحمل الاسم نفسه وصدّروها باستخدام
export -f. skip— تجاوزوا الاختبارات التي تعتمد على موارد غير متاحة بشكل مشروط.- تخطيط المشروع — افصلوا بين
lib/وtest/وtest/helpers/لتسهيل الصيانة والدمج مع CI.
تمنح هذه الأنماط مشاريع Bash لديكم الانضباط نفسه في الاختبار الذي تطبقونه على أي مشروع برمجي حديث.
الأسئلة الشائعة
هل درس «اختبار الدوال بوحدة باستخدام Bats-core» مجاني؟
نعم — نص درس «اختبار الدوال بوحدة باستخدام Bats-core» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DevOps Bootcamp، انتقل إلى CoddyKit PRO. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.
ماذا ستتعلم في «اختبار الدوال بوحدة باستخدام Bats-core»؟
نظّم ملفات الاختبار والتأكيدات وإعداد الاختبارات وإنهائها للتحقّق من دوال Bash الفردية تتمرن على DevOps Bootcamp مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DevOps Bootcamp؟
لا تُشترط خبرة سابقة. DevOps Bootcamp على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «اختبار الدوال بوحدة باستخدام Bats-core»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DevOps Bootcamp هذا؟
نعم. كل درس في DevOps Bootcamp يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- اختبار الدوال بوحدة باستخدام Bats-core
- محاكاة الأوامر وإنشاء بدائل للأدوات الخارجية
- بيانات الاختبار والبيئات المؤقتة وتغطية الاختبارات
- تشغيل اختبارات Shell في مسارات CI