{"id":24725484,"url":"https://github.com/binaryphile/tesht","last_synced_at":"2026-05-18T17:08:42.202Z","repository":{"id":273115018,"uuid":"918679477","full_name":"binaryphile/tesht","owner":"binaryphile","description":"table-driven testing for Bash","archived":false,"fork":false,"pushed_at":"2026-05-04T21:52:35.000Z","size":304,"stargazers_count":2,"open_issues_count":10,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-04T23:39:43.894Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/binaryphile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-18T15:20:20.000Z","updated_at":"2026-05-04T21:52:40.000Z","dependencies_parsed_at":"2025-01-18T19:33:09.770Z","dependency_job_id":"36e4ac2e-e998-4667-b12b-363eae0ba536","html_url":"https://github.com/binaryphile/tesht","commit_stats":null,"previous_names":["binaryphile/tesht"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/binaryphile/tesht","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftesht","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftesht/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftesht/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftesht/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/binaryphile","download_url":"https://codeload.github.com/binaryphile/tesht/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryphile%2Ftesht/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33184824,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-18T09:27:30.708Z","status":"ssl_error","status_checked_at":"2026-05-18T09:27:28.300Z","response_time":71,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-27T13:19:27.413Z","updated_at":"2026-05-18T17:08:42.197Z","avatar_url":"https://github.com/binaryphile.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tesht — Table-Driven Testing for Bash\n\n![Version](assets/version.svg)\n![Tests](assets/tests.svg)\n![Coverage](assets/coverage.svg)\n![Lines](assets/lines.svg)\n\nA lightweight testing framework for Bash scripts inspired by Go’s testing package. Write\nclean, maintainable table-driven tests for your shell functions with automatic test\ndiscovery and clear failure reporting.\n\n![tesht in action](assets/tesht.gif)\n\n## Why Test Bash?\n\n- Bash code is infrastructure and needs to be reliable\n- Prevent regressions when refactoring shell scripts\n- Verify intended behavior regardless of experience level\n- Explore and learn bash with confidence\n\n## Features\n\n- **Automatic discovery** of `*_test.bash` files and `test_*` functions\n- **Table-driven testing** with reusable test logic across multiple cases\n- **Test isolation** - each test runs in its own subshell\n- **Clear output** - detailed pass/fail status with timing and colored results\n- **Helpful assertions** with diff output and suggested fixes\n- **Built-in utilities** for HTTP servers, temp directories, and common test needs\n\n## Quick Start\n\n1.  **Install tesht**:\n\n    ``` bash\n    cp tesht /usr/local/bin/\n    # or\n    ln -s \"$PWD/tesht\" ~/bin/\n    ```\n\n2.  **Make your script testable** by adding this line after your functions:\n\n    ``` bash\n    return 2\u003e/dev/null  # allows sourcing without execution\n    ```\n\n3.  **Write a test file** (e.g., `myScript_test.bash`):\n\n    ``` bash\n    #!/usr/bin/env bash\n    source ./myScript || exit 1\n\n    test_MyFunction() {\n        local -A case1=(\n            [name]='basic functionality'\n            [input]='test input'\n            [want]='expected output'\n        )\n\n        local -A case2=(\n            [name]='edge case'\n            [input]='edge input'\n            [want]='edge output'\n        )\n\n        subtest() {\n            local casename=$1\n            eval \"$(tesht.Inherit $casename)\"\n\n            local got\n            got=$(MyFunction \"$input\")\n\n            tesht.AssertGot \"$got\" \"$want\"\n        }\n\n        tesht.Run ${!case@}\n    }\n    ```\n\n4.  **Run tests**:\n\n    ``` bash\n    tesht                    # run all tests\n    tesht test_MyFunction    # run specific test\n    tesht -f myScript_test.bash  # run tests from specific file\n    tesht test_Func1 test_Func2  # run multiple specific tests\n    ```\n\n## Writing Tests\n\n### Basic Test Structure\n\nTests are functions named `test_*` that tesht discovers automatically:\n\n``` bash\ntest_BasicFunction() {\n    ## arrange\n    local input=\"test data\"\n    local expected=\"expected result\"\n    \n    ## act\n    local result\n    result=$(MyFunction \"$input\")\n    \n    ## assert\n    tesht.AssertGot \"$result\" \"$expected\"\n}\n```\n\n### Table-Driven Tests\n\nFor multiple test cases sharing the same logic:\n\n``` bash\ntest_CalculatorTable() {\n    local -A case1=([name]='addition' [a]=2 [b]=3 [op]='+' [want]=5)\n    local -A case2=([name]='subtraction' [a]=5 [b]=2 [op]='-' [want]=3)\n    local -A case3=([name]='multiplication' [a]=4 [b]=3 [op]='*' [want]=12)\n\n    subtest() {\n        local casename=$1\n        eval \"$(tesht.Inherit $casename)\"\n        \n        local got\n        got=$(Calculator \"$a\" \"$op\" \"$b\")\n        \n        tesht.AssertGot \"$got\" \"$want\"\n    }\n\n    tesht.Run ${!case@}\n}\n```\n\n### Testing Return Codes\n\n``` bash\ntest_ErrorHandling() {\n    local got rc\n    got=$(SomeFunction \"invalid input\") \u0026\u0026 rc=$? || rc=$?\n    \n    tesht.Softly \u003c\u003c'END'\n        tesht.AssertRC $rc 1\n        tesht.AssertGot \"$got\" \"Error: invalid input\"\nEND\n}\n```\n\n### Smoke Testing CLIs\n\nFor ad-hoc CLI smoke checks outside test functions — e.g. verifying\na binary's validation paths after a build — naive shell chains\npropagate the *last* command's exit code as the script's. A script\nending with an intentional-failure probe (a command that's *supposed*\nto exit nonzero) reports an overall nonzero exit, even though the\nprobe behaved exactly as designed. `tesht.Smoke` inverts that: it\nruns a command and succeeds iff the command's exit code matches the\nexpected value.\n\n``` bash\n# After building, verify the CLI's validation paths.\ntesht.Smoke 1 mytool --required        # missing required arg → expect rc=1\ntesht.Smoke 1 mytool --bad-flag        # unknown flag → expect rc=1\ntesht.Smoke 0 mytool --version         # success path → expect rc=0\necho 'all smoke checks passed'\n```\n\nEach call returns 0 if the command's actual rc matches the expected;\nnonzero otherwise (with the actual rc and captured output reported on\nstderr via `tesht.Log`). The script's overall exit code now reflects\n*unexpected* failures only.\n\nThe optional `--` separator can disambiguate when the command starts\nwith a flag-like token: `tesht.Smoke 1 -- mytool --help`.\n\n### Mocking Commands\n\n``` bash\ntest_WithMockedCommand() {\n    # Mock external command\n    curl() {\n        echo \"mocked response\"\n        return 0\n    }\n    \n    local result\n    result=$(FunctionThatUsesCurl)\n    \n    tesht.AssertGot \"$result\" \"processed: mocked response\"\n}\n```\n\n## API Reference\n\n### Core Functions\n\n- **`tesht.Run ${!case@}`** - Execute table-driven subtests\n- **`tesht.Inherit $casename`** - Load associative array into local variables\n- **`tesht.AssertGot actual expected`** - Compare strings with diff on failure\n- **`tesht.AssertRC actual expected`** - Compare return codes\n- **`tesht.Smoke expected_rc [--] cmd [args...]`** - Run a CLI smoke check; succeed iff actual rc matches expected\n- **`tesht.Softly`** - Run multiple assertions, continue on failure\n- **`tesht.Log message...`** - Print message from test\n\n### Utilities\n\n- **`tesht.MktempDir`** - Create temporary directory (auto-cleanup)\n- **`tesht.StartHttpServer [port]`** - Start HTTP server for testing\n- **`tesht.Diff expected actual`** - Show unified diff\n\n## Usage\n\n``` bash\ntesht [-x] [-f file1,file2,...] [test_name...]\n```\n\n- **No arguments**: Runs all `test_*` functions in all `*_test.bash` files\n- **With test names**: Runs only the specified tests and their subtests\n- **`-f` flag**: Constrain execution to specific test files (comma-separated)\n- **`-x` flag**: Enable trace output for debugging test discovery and execution\n\n## Example Output\n\n    === RUN         test_Calculator/addition\n    --- PASS    2ms test_Calculator/addition\n    === RUN         test_Calculator/subtraction  \n    --- PASS    1ms test_Calculator/subtraction\n    === RUN         test_Calculator/division by zero\n\n    got does not match want:\n    \u003c Error: division by zero\n    ---\n    \u003e Expected error message\n\n    use this line to update want to match:\n        want='Error: division by zero'\n\n    --- FAIL    3ms test_Calculator/division by zero\n    FAIL        6ms\n    1/3\n\n## Test Isolation\n\nEach test function runs inside a subshell (`( ... )`), forked from the\nrunner. This gives strong process-level isolation between tests.\n\n**Cannot leak between tests:**\n\n- Variable changes (`local`, `export`, plain assignments)\n- Working directory (`cd`)\n- Shell options (`set -e`, `set -o noglob`, ...)\n- Function redefinitions, sourced files\n- `EXIT` / `ERR` / signal traps\n\n**Cannot affect the parent test runner:**\n\n- `exec` replaces only the subshell's process; the runner is the parent\n  and is unaffected\n- `exit` terminates only the subshell; the runner records the rc and\n  continues\n- Resource limits (`ulimit`) apply only inside the subshell\n\n**Can leak — use the cleanup helpers:**\n\n- Filesystem changes outside a temp dir → use `tesht.MktempDir`\n- Background child processes → register cleanup with `tesht.Defer` (or a\n  `trap \"kill $pid\" EXIT` for single-test scripts)\n- External state (network listeners, shared databases, system files)\n\n**Genuinely shared:**\n\n- The terminal `stdout`/`stderr` (the runner serializes test output)\n- Resources reachable through `kill $PPID` or `/proc/$PPID/...` —\n  isolation only protects against ordinary test misbehavior, not\n  deliberate sabotage\n\nThe practical guarantee: write each test assuming a clean process state\n(it gets one), but treat the filesystem and external resources as shared\nand clean them up explicitly.\n\n## Best Practices\n\n1.  **Test file naming**: Use `*_test.bash` suffix matching the script name\n2.  **Test function naming**: Prefix with `test_` for auto-discovery\n3.  **Case naming**: Use descriptive names that explain what’s being tested\n4.  **Arrange-Act-Assert**: Structure tests with clear sections\n5.  **Edge cases**: Test boundary conditions and error scenarios\n6.  **Isolation**: Don’t rely on test execution order\n7.  **Cleanup**: Use temp directories and trap for resource cleanup\n\n## Real-World Example\n\n``` bash\n# test_HttpClient tests HTTP request functionality\ntest_HttpClient() {\n    ## arrange\n    local dir\n    dir=$(tesht.MktempDir) || return 128\n    cd \"$dir\"\n    \n    echo \"test content\" \u003e index.html\n    \n    local pid\n    pid=$(tesht.StartHttpServer 8080) || return 128\n    trap \"kill $pid\" EXIT\n    \n    ## act\n    local response rc\n    response=$(HttpGet \"http://localhost:8080/index.html\") \u0026\u0026 rc=$? || rc=$?\n    \n    ## assert\n    tesht.Softly \u003c\u003c'END'\n        tesht.AssertRC $rc 0\n        tesht.AssertGot \"$response\" \"test content\"\nEND\n}\n```\n\n## License\n\nMIT License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryphile%2Ftesht","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinaryphile%2Ftesht","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryphile%2Ftesht/lists"}