bats-testing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Bats Testing Patterns

Bats测试模式

Comprehensive guidance for writing comprehensive unit tests for shell scripts using Bats (Bash Automated Testing System), including test patterns, fixtures, and best practices for production-grade shell testing.
本文提供了使用Bats(Bash自动化测试系统)编写全面Shell脚本单元测试的综合指南,包括测试模式、测试夹具以及生产级Shell测试的最佳实践。

When to Use This Skill

何时使用该技能

  • Writing unit tests for shell scripts
  • Implementing test-driven development (TDD) for scripts
  • Setting up automated testing in CI/CD pipelines
  • Testing edge cases and error conditions
  • Validating behavior across different shell environments
  • Building maintainable test suites for scripts
  • Creating fixtures for complex test scenarios
  • Testing multiple shell dialects (bash, sh, dash)
  • 为Shell脚本编写单元测试
  • 为脚本实现测试驱动开发(TDD)
  • 在CI/CD流水线中设置自动化测试
  • 测试边缘情况和错误条件
  • 在不同Shell环境中验证行为
  • 为脚本构建可维护的测试套件
  • 为复杂测试场景创建测试夹具
  • 测试多种Shell方言(bash、sh、dash)

Bats Fundamentals

Bats基础

What is Bats?

什么是Bats?

Bats (Bash Automated Testing System) is a TAP (Test Anything Protocol) compliant testing framework for shell scripts that provides:
  • Simple, natural test syntax
  • TAP output format compatible with CI systems
  • Fixtures and setup/teardown support
  • Assertion helpers
  • Parallel test execution
Bats(Bash自动化测试系统)是一个兼容TAP(测试任何协议)的Shell脚本测试框架,提供以下功能:
  • 简洁、自然的测试语法
  • 与CI系统兼容的TAP输出格式
  • 测试夹具和初始化/清理支持
  • 断言辅助工具
  • 并行测试执行

Installation

安装

bash
undefined
bash
undefined

macOS with Homebrew

macOS使用Homebrew

brew install bats-core
brew install bats-core

Ubuntu/Debian

Ubuntu/Debian

git clone https://github.com/bats-core/bats-core.git cd bats-core ./install.sh /usr/local
git clone https://github.com/bats-core/bats-core.git cd bats-core ./install.sh /usr/local

From npm (Node.js)

从npm(Node.js)安装

npm install --global bats
npm install --global bats

Verify installation

验证安装

bats --version
undefined
bats --version
undefined

File Structure

文件结构

project/
├── bin/
│   ├── script.sh
│   └── helper.sh
├── tests/
│   ├── test_script.bats
│   ├── test_helper.sh
│   ├── fixtures/
│   │   ├── input.txt
│   │   └── expected_output.txt
│   └── helpers/
│       └── mocks.bash
└── README.md
project/
├── bin/
│   ├── script.sh
│   └── helper.sh
├── tests/
│   ├── test_script.bats
│   ├── test_helper.sh
│   ├── fixtures/
│   │   ├── input.txt
│   │   └── expected_output.txt
│   └── helpers/
│       └── mocks.bash
└── README.md

Basic Test Structure

基础测试结构

Simple Test File

简单测试文件

bash
#!/usr/bin/env bats
bash
#!/usr/bin/env bats

Load test helper if present

加载测试辅助工具(如果存在)

load test_helper
load test_helper

Setup runs before each test

Setup会在每个测试前运行

setup() { export TMPDIR=$(mktemp -d) }
setup() { export TMPDIR=$(mktemp -d) }

Teardown runs after each test

Teardown会在每个测试后运行

teardown() { rm -rf "$TMPDIR" }
teardown() { rm -rf "$TMPDIR" }

Test: simple assertion

测试:简单断言

@test "Function returns 0 on success" { run my_function "input" [ "$status" -eq 0 ] }
@test "函数执行成功时返回0" { run my_function "input" [ "$status" -eq 0 ] }

Test: output verification

测试:输出验证

@test "Function outputs correct result" { run my_function "test" [ "$output" = "expected output" ] }
@test "函数输出正确结果" { run my_function "test" [ "$output" = "expected output" ] }

Test: error handling

测试:错误处理

@test "Function returns 1 on missing argument" { run my_function [ "$status" -eq 1 ] }
undefined
@test "函数在缺少参数时返回1" { run my_function [ "$status" -eq 1 ] }
undefined

Assertion Patterns

断言模式

Exit Code Assertions

退出码断言

bash
#!/usr/bin/env bats

@test "Command succeeds" {
    run true
    [ "$status" -eq 0 ]
}

@test "Command fails as expected" {
    run false
    [ "$status" -ne 0 ]
}

@test "Command returns specific exit code" {
    run my_function --invalid
    [ "$status" -eq 127 ]
}

@test "Can capture command result" {
    run echo "hello"
    [ $status -eq 0 ]
    [ "$output" = "hello" ]
}
bash
#!/usr/bin/env bats

@test "命令执行成功" {
    run true
    [ "$status" -eq 0 ]
}

@test "命令按预期执行失败" {
    run false
    [ "$status" -ne 0 ]
}

@test "命令返回特定退出码" {
    run my_function --invalid
    [ "$status" -eq 127 ]
}

@test "可以捕获命令结果" {
    run echo "hello"
    [ $status -eq 0 ]
    [ "$output" = "hello" ]
}

Output Assertions

输出断言

bash
#!/usr/bin/env bats

@test "Output matches string" {
    result=$(echo "hello world")
    [ "$result" = "hello world" ]
}

@test "Output contains substring" {
    result=$(echo "hello world")
    [[ "$result" == *"world"* ]]
}

@test "Output matches pattern" {
    result=$(date +%Y)
    [[ "$result" =~ ^[0-9]{4}$ ]]
}

@test "Multi-line output" {
    run printf "line1\nline2\nline3"
    [ "$output" = "line1
line2
line3" ]
}

@test "Lines variable contains output" {
    run printf "line1\nline2\nline3"
    [ "${lines[0]}" = "line1" ]
    [ "${lines[1]}" = "line2" ]
    [ "${lines[2]}" = "line3" ]
}
bash
#!/usr/bin/env bats

@test "输出匹配指定字符串" {
    result=$(echo "hello world")
    [ "$result" = "hello world" ]
}

@test "输出包含子字符串" {
    result=$(echo "hello world")
    [[ "$result" == *"world"* ]]
}

@test "输出匹配指定模式" {
    result=$(date +%Y)
    [[ "$result" =~ ^[0-9]{4}$ ]]
}

@test "多行输出" {
    run printf "line1\nline2\nline3"
    [ "$output" = "line1
line2
line3" ]
}

@test "Lines变量包含输出内容" {
    run printf "line1\nline2\nline3"
    [ "${lines[0]}" = "line1" ]
    [ "${lines[1]}" = "line2" ]
    [ "${lines[2]}" = "line3" ]
}

File Assertions

文件断言

bash
#!/usr/bin/env bats

@test "File is created" {
    [ ! -f "$TMPDIR/output.txt" ]
    my_function > "$TMPDIR/output.txt"
    [ -f "$TMPDIR/output.txt" ]
}

@test "File contents match expected" {
    my_function > "$TMPDIR/output.txt"
    [ "$(cat "$TMPDIR/output.txt")" = "expected content" ]
}

@test "File is readable" {
    touch "$TMPDIR/test.txt"
    [ -r "$TMPDIR/test.txt" ]
}

@test "File has correct permissions" {
    touch "$TMPDIR/test.txt"
    chmod 644 "$TMPDIR/test.txt"
    [ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ]
}

@test "File size is correct" {
    echo -n "12345" > "$TMPDIR/test.txt"
    [ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ]
}
bash
#!/usr/bin/env bats

@test "文件已创建" {
    [ ! -f "$TMPDIR/output.txt" ]
    my_function > "$TMPDIR/output.txt"
    [ -f "$TMPDIR/output.txt" ]
}

@test "文件内容与预期匹配" {
    my_function > "$TMPDIR/output.txt"
    [ "$(cat "$TMPDIR/output.txt")" = "expected content" ]
}

@test "文件可读" {
    touch "$TMPDIR/test.txt"
    [ -r "$TMPDIR/test.txt" ]
}

@test "文件权限正确" {
    touch "$TMPDIR/test.txt"
    chmod 644 "$TMPDIR/test.txt"
    [ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ]
}

@test "文件大小正确" {
    echo -n "12345" > "$TMPDIR/test.txt"
    [ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ]
}

Setup and Teardown Patterns

初始化和清理模式

Basic Setup and Teardown

基础初始化和清理

bash
#!/usr/bin/env bats

setup() {
    # Create test directory
    TEST_DIR=$(mktemp -d)
    export TEST_DIR

    # Source script under test
    source "${BATS_TEST_DIRNAME}/../bin/script.sh"
}

teardown() {
    # Clean up temporary directory
    rm -rf "$TEST_DIR"
}

@test "Test using TEST_DIR" {
    touch "$TEST_DIR/file.txt"
    [ -f "$TEST_DIR/file.txt" ]
}
bash
#!/usr/bin/env bats

setup() {
    # 创建测试目录
    TEST_DIR=$(mktemp -d)
    export TEST_DIR

    # 引入待测试的脚本
    source "${BATS_TEST_DIRNAME}/../bin/script.sh"
}

teardown() {
    # 清理临时目录
    rm -rf "$TEST_DIR"
}

@test "使用TEST_DIR进行测试" {
    touch "$TEST_DIR/file.txt"
    [ -f "$TEST_DIR/file.txt" ]
}

Setup with Resources

带资源的初始化

bash
#!/usr/bin/env bats

setup() {
    # Create directory structure
    mkdir -p "$TMPDIR/data/input"
    mkdir -p "$TMPDIR/data/output"

    # Create test fixtures
    echo "line1" > "$TMPDIR/data/input/file1.txt"
    echo "line2" > "$TMPDIR/data/input/file2.txt"

    # Initialize environment
    export DATA_DIR="$TMPDIR/data"
    export INPUT_DIR="$DATA_DIR/input"
    export OUTPUT_DIR="$DATA_DIR/output"
}

teardown() {
    rm -rf "$TMPDIR/data"
}

@test "Processes input files" {
    run my_process_script "$INPUT_DIR" "$OUTPUT_DIR"
    [ "$status" -eq 0 ]
    [ -f "$OUTPUT_DIR/file1.txt" ]
}
bash
#!/usr/bin/env bats

setup() {
    # 创建目录结构
    mkdir -p "$TMPDIR/data/input"
    mkdir -p "$TMPDIR/data/output"

    # 创建测试夹具
    echo "line1" > "$TMPDIR/data/input/file1.txt"
    echo "line2" > "$TMPDIR/data/input/file2.txt"

    # 初始化环境变量
    export DATA_DIR="$TMPDIR/data"
    export INPUT_DIR="$DATA_DIR/input"
    export OUTPUT_DIR="$DATA_DIR/output"
}

teardown() {
    rm -rf "$TMPDIR/data"
}

@test "处理输入文件" {
    run my_process_script "$INPUT_DIR" "$OUTPUT_DIR"
    [ "$status" -eq 0 ]
    [ -f "$OUTPUT_DIR/file1.txt" ]
}

Global Setup/Teardown

全局初始化/清理

bash
#!/usr/bin/env bats
bash
#!/usr/bin/env bats

Load shared setup from test_helper.sh

从test_helper.sh加载共享初始化配置

load test_helper
load test_helper

setup_file runs once before all tests

setup_file会在所有测试前运行一次

setup_file() { export SHARED_RESOURCE=$(mktemp -d) echo "Expensive setup" > "$SHARED_RESOURCE/data.txt" }
setup_file() { export SHARED_RESOURCE=$(mktemp -d) echo "Expensive setup" > "$SHARED_RESOURCE/data.txt" }

teardown_file runs once after all tests

teardown_file会在所有测试后运行一次

teardown_file() { rm -rf "$SHARED_RESOURCE" }
@test "First test uses shared resource" { [ -f "$SHARED_RESOURCE/data.txt" ] }
@test "Second test uses shared resource" { [ -d "$SHARED_RESOURCE" ] }
undefined
teardown_file() { rm -rf "$SHARED_RESOURCE" }
@test "第一个测试使用共享资源" { [ -f "$SHARED_RESOURCE/data.txt" ] }
@test "第二个测试使用共享资源" { [ -d "$SHARED_RESOURCE" ] }
undefined

Mocking and Stubbing Patterns

模拟和存根模式

Function Mocking

函数模拟

bash
#!/usr/bin/env bats
bash
#!/usr/bin/env bats

Mock external command

模拟外部命令

my_external_tool() { echo "mocked output" return 0 }
@test "Function uses mocked tool" { export -f my_external_tool run my_function [[ "$output" == "mocked output" ]] }
undefined
my_external_tool() { echo "mocked output" return 0 }
@test "函数使用模拟工具" { export -f my_external_tool run my_function [[ "$output" == "mocked output" ]] }
undefined

Command Stubbing

命令存根

bash
#!/usr/bin/env bats

setup() {
    # Create stub directory
    STUBS_DIR="$TMPDIR/stubs"
    mkdir -p "$STUBS_DIR"

    # Add to PATH
    export PATH="$STUBS_DIR:$PATH"
}

create_stub() {
    local cmd="$1"
    local output="$2"
    local code="${3:-0}"

    cat > "$STUBS_DIR/$cmd" <<EOF
#!/bin/bash
echo "$output"
exit $code
EOF
    chmod +x "$STUBS_DIR/$cmd"
}

@test "Function works with stubbed curl" {
    create_stub curl "{ \"status\": \"ok\" }" 0
    run my_api_function
    [ "$status" -eq 0 ]
}
bash
#!/usr/bin/env bats

setup() {
    # 创建存根目录
    STUBS_DIR="$TMPDIR/stubs"
    mkdir -p "$STUBS_DIR"

    # 添加到PATH环境变量
    export PATH="$STUBS_DIR:$PATH"
}

create_stub() {
    local cmd="$1"
    local output="$2"
    local code="${3:-0}"

    cat > "$STUBS_DIR/$cmd" <<EOF
#!/bin/bash
echo "$output"
exit $code
EOF
    chmod +x "$STUBS_DIR/$cmd"
}

@test "函数配合存根的curl使用" {
    create_stub curl "{ \"status\": \"ok\" }" 0
    run my_api_function
    [ "$status" -eq 0 ]
}

Variable Stubbing

变量存根

bash
#!/usr/bin/env bats

@test "Function handles environment override" {
    export MY_SETTING="override_value"
    run my_function
    [ "$status" -eq 0 ]
    [[ "$output" == *"override_value"* ]]
}

@test "Function uses default when var unset" {
    unset MY_SETTING
    run my_function
    [ "$status" -eq 0 ]
    [[ "$output" == *"default"* ]]
}
bash
#!/usr/bin/env bats

@test "函数处理环境变量覆盖" {
    export MY_SETTING="override_value"
    run my_function
    [ "$status" -eq 0 ]
    [[ "$output" == *"override_value"* ]]
}

@test "变量未设置时函数使用默认值" {
    unset MY_SETTING
    run my_function
    [ "$status" -eq 0 ]
    [[ "$output" == *"default"* ]]
}

Fixture Management

测试夹具管理

Using Fixture Files

使用夹具文件

bash
#!/usr/bin/env bats
bash
#!/usr/bin/env bats

Fixture directory: tests/fixtures/

夹具目录:tests/fixtures/

setup() { FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures" WORK_DIR=$(mktemp -d) export WORK_DIR }
teardown() { rm -rf "$WORK_DIR" }
@test "Process fixture file" { # Copy fixture to work directory cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"
# Run function
run my_process_function "$WORK_DIR/input.txt"

# Compare output
diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"
}
undefined
setup() { FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures" WORK_DIR=$(mktemp -d) export WORK_DIR }
teardown() { rm -rf "$WORK_DIR" }
@test "处理夹具文件" { # 将夹具复制到工作目录 cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"
# 运行函数
run my_process_function "$WORK_DIR/input.txt"

# 比较输出
diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"
}
undefined

Dynamic Fixture Generation

动态生成夹具

bash
#!/usr/bin/env bats

generate_fixture() {
    local lines="$1"
    local file="$2"

    for i in $(seq 1 "$lines"); do
        echo "Line $i content" >> "$file"
    done
}

@test "Handle large input file" {
    generate_fixture 1000 "$TMPDIR/large.txt"
    run my_function "$TMPDIR/large.txt"
    [ "$status" -eq 0 ]
    [ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ]
}
bash
#!/usr/bin/env bats

generate_fixture() {
    local lines="$1"
    local file="$2"

    for i in $(seq 1 "$lines"); do
        echo "Line $i content" >> "$file"
    done
}

@test "处理大型输入文件" {
    generate_fixture 1000 "$TMPDIR/large.txt"
    run my_function "$TMPDIR/large.txt"
    [ "$status" -eq 0 ]
    [ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ]
}

Advanced Patterns

高级模式

Testing Error Conditions

测试错误条件

bash
#!/usr/bin/env bats

@test "Function fails with missing file" {
    run my_function "/nonexistent/file.txt"
    [ "$status" -ne 0 ]
    [[ "$output" == *"not found"* ]]
}

@test "Function fails with invalid input" {
    run my_function ""
    [ "$status" -ne 0 ]
}

@test "Function fails with permission denied" {
    touch "$TMPDIR/readonly.txt"
    chmod 000 "$TMPDIR/readonly.txt"
    run my_function "$TMPDIR/readonly.txt"
    [ "$status" -ne 0 ]
    chmod 644 "$TMPDIR/readonly.txt"  # Cleanup
}

@test "Function provides helpful error message" {
    run my_function --invalid-option
    [ "$status" -ne 0 ]
    [[ "$output" == *"Usage:"* ]]
}
bash
#!/usr/bin/env bats

@test "函数在文件不存在时执行失败" {
    run my_function "/nonexistent/file.txt"
    [ "$status" -ne 0 ]
    [[ "$output" == *"not found"* ]]
}

@test "函数在输入为空时执行失败" {
    run my_function ""
    [ "$status" -ne 0 ]
}

@test "函数在权限不足时执行失败" {
    touch "$TMPDIR/readonly.txt"
    chmod 000 "$TMPDIR/readonly.txt"
    run my_function "$TMPDIR/readonly.txt"
    [ "$status" -ne 0 ]
    chmod 644 "$TMPDIR/readonly.txt"  # 清理
}

@test "函数提供有用的错误信息" {
    run my_function --invalid-option
    [ "$status" -ne 0 ]
    [[ "$output" == *"Usage:"* ]]
}

Testing with Dependencies

带依赖的测试

bash
#!/usr/bin/env bats

setup() {
    # Check for required tools
    if ! command -v jq &>/dev/null; then
        skip "jq is not installed"
    fi

    export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
}

@test "JSON parsing works" {
    skip_if ! command -v jq &>/dev/null
    run my_json_parser '{"key": "value"}'
    [ "$status" -eq 0 ]
}
bash
#!/usr/bin/env bats

setup() {
    # 检查是否存在所需工具
    if ! command -v jq &>/dev/null; then
        skip "jq未安装"
    fi

    export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
}

@test "JSON解析功能正常" {
    skip_if ! command -v jq &>/dev/null
    run my_json_parser '{"key": "value"}'
    [ "$status" -eq 0 ]
}

Testing Shell Compatibility

测试Shell兼容性

bash
#!/usr/bin/env bats

@test "Script works in bash" {
    bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "Script works in sh (POSIX)" {
    sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "Script works in dash" {
    if command -v dash &>/dev/null; then
        dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
    else
        skip "dash not installed"
    fi
}
bash
#!/usr/bin/env bats

@test "脚本在bash中正常运行" {
    bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "脚本在sh(POSIX)中正常运行" {
    sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}

@test "脚本在dash中正常运行" {
    if command -v dash &>/dev/null; then
        dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
    else
        skip "dash未安装"
    fi
}

Parallel Execution

并行执行

bash
#!/usr/bin/env bats

@test "Multiple independent operations" {
    run bash -c 'for i in {1..10}; do
        my_operation "$i" &
    done
    wait'
    [ "$status" -eq 0 ]
}

@test "Concurrent file operations" {
    for i in {1..5}; do
        my_function "$TMPDIR/file$i" &
    done
    wait
    [ -f "$TMPDIR/file1" ]
    [ -f "$TMPDIR/file5" ]
}
bash
#!/usr/bin/env bats

@test "多个独立操作" {
    run bash -c 'for i in {1..10}; do
        my_operation "$i" &
    done
    wait'
    [ "$status" -eq 0 ]
}

@test "并发文件操作" {
    for i in {1..5}; do
        my_function "$TMPDIR/file$i" &
    done
    wait
    [ -f "$TMPDIR/file1" ]
    [ -f "$TMPDIR/file5" ]
}

Test Helper Pattern

测试辅助工具模式

test_helper.sh

test_helper.sh

bash
#!/usr/bin/env bash
bash
#!/usr/bin/env bash

Source script under test

引入待测试的脚本

export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"
export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"

Common test utilities

通用测试工具

assert_file_exists() { if [ ! -f "$1" ]; then echo "Expected file to exist: $1" return 1 fi }
assert_file_equals() { local file="$1" local expected="$2"
if [ ! -f "$file" ]; then
    echo "File does not exist: $file"
    return 1
fi

local actual=$(cat "$file")
if [ "$actual" != "$expected" ]; then
    echo "File contents do not match"
    echo "Expected: $expected"
    echo "Actual: $actual"
    return 1
fi
}
assert_file_exists() { if [ ! -f "$1" ]; then echo "预期文件存在:$1" return 1 fi }
assert_file_equals() { local file="$1" local expected="$2"
if [ ! -f "$file" ]; then
    echo "文件不存在:$file"
    return 1
fi

local actual=$(cat "$file")
if [ "$actual" != "$expected" ]; then
    echo "文件内容不匹配"
    echo "预期:$expected"
    echo "实际:$actual"
    return 1
fi
}

Create temporary test directory

创建临时测试目录

setup_test_dir() { export TEST_DIR=$(mktemp -d) }
cleanup_test_dir() { rm -rf "$TEST_DIR" }
undefined
setup_test_dir() { export TEST_DIR=$(mktemp -d) }

Integration with CI/CD

清理临时测试目录

GitHub Actions Workflow

yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Install Bats
        run: |
          npm install --global bats

      - name: Run Tests
        run: |
          bats tests/*.bats

      - name: Run Tests with Tap Reporter
        run: |
          bats tests/*.bats --tap | tee test_output.tap
cleanup_test_dir() { rm -rf "$TEST_DIR" }
undefined

Makefile Integration

与CI/CD集成

GitHub Actions工作流

makefile
.PHONY: test test-verbose test-tap

test:
	bats tests/*.bats

test-verbose:
	bats tests/*.bats --verbose

test-tap:
	bats tests/*.bats --tap

test-parallel:
	bats tests/*.bats --parallel 4

coverage: test
	# Optional: Generate coverage reports
yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: 安装Bats
        run: |
          npm install --global bats

      - name: 运行测试
        run: |
          bats tests/*.bats

      - name: 使用Tap报告器运行测试
        run: |
          bats tests/*.bats --tap | tee test_output.tap

Best Practices

Makefile集成

  1. Test one thing per test - Single responsibility principle
  2. Use descriptive test names - Clearly states what is being tested
  3. Clean up after tests - Always remove temporary files in teardown
  4. Test both success and failure paths - Don't just test happy path
  5. Mock external dependencies - Isolate unit under test
  6. Use fixtures for complex data - Makes tests more readable
  7. Run tests in CI/CD - Catch regressions early
  8. Test across shell dialects - Ensure portability
  9. Keep tests fast - Run in parallel when possible
  10. Document complex test setup - Explain unusual patterns
makefile
.PHONY: test test-verbose test-tap

test:
	bats tests/*.bats

test-verbose:
	bats tests/*.bats --verbose

test-tap:
	bats tests/*.bats --tap

test-parallel:
	bats tests/*.bats --parallel 4

coverage: test
	# 可选:生成覆盖率报告

Resources

最佳实践

  1. 每个测试只测试一件事 - 单一职责原则
  2. 使用描述性的测试名称 - 清晰说明测试内容
  3. 测试后清理 - 始终在teardown中删除临时文件
  4. 同时测试成功和失败路径 - 不要只测试正常路径
  5. 模拟外部依赖 - 隔离待测试单元
  6. 使用夹具处理复杂数据 - 让测试更易读
  7. 在CI/CD中运行测试 - 尽早发现回归问题
  8. 跨Shell方言测试 - 确保可移植性
  9. 保持测试快速 - 尽可能并行运行
  10. 记录复杂的测试设置 - 解释不常见的模式

资源