bashunit

repository·main·Indexed 19 days ago

https://github.com/typeddevs/bashunit

A lightweight, fast testing framework for Bash 3.0+ scripts. It provides assertions, mocks, spies, snapshot testing, and branch coverage. Features include parallel test execution via the --parallel flag, LCOV-compatible coverage output, and multiple coverage tracing engines (xtrace and trap) to balance performance and compatibility.

Tokens
52.7K
Snippets
233
Records
300
Agent score
63%

What's inside bashunit

  1. Overview of bashunit features

    main

    bashunit is a fast and simple testing framework designed for Bash scripts (compatible with Bash 3.0+ on Linux, macOS, and WSL). It provides a modern suite of testing tools including:

    • Assertions: Robust tools for comparing, matching, and validating results.
    • Mocks and Spies: For isolating code under test.
    • Data Providers: To run tests against multiple sets of data.
    • Snapshots: For verifying output consistency.
    • Coverage: To measure how much of your code is exercised by tests.
  2. Bashunit CLI command overview

    main

    bashunit uses a subcommand-based CLI architecture. Each subcommand has its own specific options and behaviors. The primary subcommands are:

    • test: Runs your bash tests (the default action).
    • bench: Runs benchmarks.
    • watch: Watches files and re-runs tests automatically when changes are detected.
    • assert: Runs a standalone assertion.
    • doc: Displays documentation for assertions.
    • init: Initializes a test directory.
    • learn: Starts an interactive tutorial.
    • upgrade: Upgrades bashunit to the latest version.
    • --help: Displays help information.
    • --version: Displays the current version.
    bashunit test [path] [options]    # Run tests (default)
    bashunit bench [path] [options]   # Run benchmarks
    bashunit watch [path] [options]   # Watch files, re-run tests on change
    bashunit assert <fn> <args>       # Run standalone assertion
    bashunit doc [options] [filter]   # Show assertion documentation
    bashunit init [dir]               # Initialize test directory
    bashunit learn                    # Interactive tutorial
    bashunit upgrade                  # Upgrade to latest version
    bashunit --help                   # Show help
    bashunit --version                # Show version
  3. Understand bashunit argument notation

    main

    When reading bashunit command documentation, arguments are denoted using specific syntax:

    • <arg>: A required argument that must be provided.
    • [arg]: An optional argument that can be omitted (the command will use a default value if omitted).
  4. Understand the bashunit source modules

    main

    The bashunit library is organized into functional modules located in the src/ directory. Each module uses an index.sh entry point to source its constituent files. Key modules include:

    • api: The public surface for test files (e.g., temp_file, skip/todo, custom-assert helpers).
    • assert: Contains all built-in assertions.
    • doubles: Provides spies and mocks.
    • config: Manages BASHUNIT_* defaults, scratch directories, parallel mode, and the rerun cache.
    • cli: Provides subcommands like doc, init, upgrade, and watch.
    • reports: Handles output formats like JUnit, TAP, JSON, GitHub Actions, and HTML.
    • runner: Manages the file loop, per-test execution, retries, and result parsing.
    • coverage: Handles line and branch tracking and coverage reports.
    • system: OS detection and I/O helpers.
    • util: String, arithmetic, and time computation.
    • state: Manages counters, per-test context, and result payloads.
    • console: Manages all printed output, including palettes and headers.
    • helper: Handles naming, test discovery, data providers, tags, and encoding.
    • main: Handles flag parsing per subcommand and the run lifecycle.
    • benchmark: Implements benchmarking functionality.
    • learn: Provides the interactive tutorial.
    • dev: Debugging helpers (excluded from released binaries).
  5. Testing individual functions vs complete scripts

    main

    Depending on how your code is structured, you should choose between sourcing the file or executing it as a standalone process.

    Testing Individual Functions

    If your script contains functions you want to test in isolation, source the script within a set_up function. This makes the functions available in the test environment's scope.

    Testing Complete Scripts

    If your script executes logic directly (e.g., a CLI tool), treat it as an executable. Run it using bash path/to/script.sh and capture its output to verify behavior.

    # Testing functions by sourcing
    function set_up() {
      source "src/calculator.sh"
    }
    
    function test_add() {
      assert_same "5" "$(add 2 3)"
    }
    
    # Testing scripts by executing
    function test_deploy() {
      local output
      output=$(bash src/deploy.sh production)
      assert_contains "Deploying to production" "$output"
    }
  6. How bashunit handles terminal colors and screen clearing

    main

    bashunit uses a hybrid approach for terminal output to balance portability and reliability:

    1. Colors: Uses hardcoded ANSI escape sequences via bashunit::sgr and _BASHUNIT_COLOR_* constants. This ensures colors work even on environments where tput might fail (like TERM=dumb runners).
    2. Screen Clearing: Uses bashunit::io::clear_screen, which attempts to use tput clear but falls back to the ANSI sequence \033[2J\033[H if tput is unavailable or returns no output. This is used specifically for --watch mode.
    3. Color Detection: The internal function bashunit::env::supports_color is used to probe terminal capabilities, returning false if TERM=dumb or if tput colors is less than 8.
  7. Write custom assertions in bashunit

    main
    Custom assertions allow you to extend bashunit with reusable, domain-specific checks. When using the bashunit facade, custom assertions automatically respect guard behavior: if a previous assertion in the same test fails, subsequent assertions are skipped. Additionally, custom assertions automatically display the correct test function name in failure messages rather than the name of the custom assertion itself, making it easy to identify the failing test.
  8. Coverage Limitations: Branching and Logic

    main

    There are specific edge cases in how bashunit calculates coverage:

    • Empty Arms: An arm containing only comments or braces (no executable lines) is reported as not-taken even if the conditional was triggered.
    • Implicit Else: In an if/elif chain without an explicit else, the synthetic fall-through outcome is not tracked.
    • Compound Conditionals: if A && B is treated as a single binary decision rather than tracking individual sub-expressions.
    • Short-circuiting: && and || operators used outside of if statements or loop-entry decisions are not tracked.
  9. Naming test functions

    main

    bashunit executes all functions within a test file that are prefixed with the word test.

    Key rules:

    • Function names must start with test to be recognized as tests.
    • Function names are case-insensitive.
    • You can use any valid Bash syntax to define these functions.

    Functions without the test prefix are treated as auxiliary functions and will not be executed as individual tests.

    function test_should_validate_an_ok_exit_code() { ... }
    function testRenderAllTestsPassedWhenNotFailedTests { ... }
    test_getFunctionsToRun_with_filter_should_return_matching_functions() { ... }
  10. Module directory structure for bashunit source files

    main

    The bashunit source code uses a directory-based module pattern to organize large files into manageable units. Instead of a single large file or a flat list of prefixed files, modules are organized into self-contained directories. Each module must contain an index.sh file, which acts as the entry point (aggregator) for that module.

    Key characteristics of this pattern:

    • Predictable Entry Points: The aggregator for any module is always located at src/*/index.sh.
    • Self-Contained Units: Modules are moved or removed as a single directory (e.g., mv src/module_name/ src/new_location/), preventing orphaned files.
    • Discovery: The build system discovers these modules by globbing the src/*/index.sh pattern.
    • Convention: This follows standard ecosystem patterns like index.ts or mod.rs.