{"id":18074513,"url":"https://github.com/codehearts/shpy","last_synced_at":"2025-04-12T05:52:51.089Z","repository":{"id":15619588,"uuid":"18356113","full_name":"codehearts/shpy","owner":"codehearts","description":"🕵️‍♀️ POSIX compliant spies and stubs for shell unit testing","archived":false,"fork":false,"pushed_at":"2025-02-14T19:50:05.000Z","size":160,"stargazers_count":13,"open_issues_count":12,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-12T05:52:43.282Z","etag":null,"topics":["hacktoberfest","mocks","posix","shell","shunit2","stubs","testing"],"latest_commit_sha":null,"homepage":"https://hub.docker.com/r/shpy/shpy","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/codehearts.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","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}},"created_at":"2014-04-02T05:54:48.000Z","updated_at":"2023-09-08T16:46:42.000Z","dependencies_parsed_at":"2024-05-22T20:41:26.426Z","dependency_job_id":"1693cb98-208e-432e-a15e-6e6e6e3d4329","html_url":"https://github.com/codehearts/shpy","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehearts%2Fshpy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehearts%2Fshpy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehearts%2Fshpy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codehearts%2Fshpy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codehearts","download_url":"https://codeload.github.com/codehearts/shpy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248525156,"owners_count":21118616,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["hacktoberfest","mocks","posix","shell","shunit2","stubs","testing"],"created_at":"2024-10-31T10:13:20.433Z","updated_at":"2025-04-12T05:52:51.065Z","avatar_url":"https://github.com/codehearts.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# shpy\n\n[![Build Status][build-badge]][build-link]\n[![Coverage][coverage-badge]][coverage-link]\n[![MIT License][license-badge]](https://github.com/codehearts/shpy/blob/master/LICENSE.md)\n![Build status for supported shells](http://github-actions.40ants.com/codehearts/shpy/matrix.svg?only=Test.test)\n\nPOSIX compliant[\u003csup\u003e[1]\u003c/sup\u003e](https://github.com/codehearts/shpy#a-word-on-shell-portability) spies and stubs for shell unit testing\n\nFeatures at a glance:\n\n- Create spies for any command or function in the shell environment\n- Stub the stdout, stderr, and return value of spies\n- See the call count and check arguments passed to spies\n- [Integrates](#shunit2-integration) with the [shunit2](https://github.com/kward/shunit2) testing framework\n\n## Table of Contents\n\n- [Why Unit Test Shell Scripts?](#why-unit-test-shell-scripts)\n- [Docker Image](#docker-image)\n- [Usage](#usage)\n- [Contributing](#contributing)\n- [API Reference](#api-reference)\n  - [shunit2 Integration](#shunit2-integration)\n- [A Word On Shell Portability](#a-word-on-shell-portability)\n\n## Why Unit Test Shell Scripts?\n\nLike other scripting languages, shell scripts can become complex and difficult to maintain over time. Unit tests help to avoid regressions and verify the correctness of functionality, but where do spies come in?\n\nSpies are useful for limiting the dependencies and scope of a test. Code that utilizes system binaries or shell functions can be tested without running the underlying implementations, allowing tests to focus solely on the system under test. To see this in action, see [examples/renamer](https://github.com/codehearts/shpy/blob/master/examples/renamer)\n\nThe benefits of spies are even greater when testing code that relies on a network. For an example of using spies to stub `curl` and make unit tests completely offline, see [examples/coverfetch](https://github.com/codehearts/shpy/blob/master/examples/coverfetch)\n\n## Docker Image\n\nShpy is available as [shpy/shpy](https://hub.docker.com/r/shpy/shpy) on Docker Hub. The latest master node is published as `shpy/shpy:latest`, while tagged releases are available as `shpy/shpy:1.0.0`. To use `kcov`, append `-kcov` to the tag or use the `kcov` tag for the latest master node\n\nTo use the shpy image, mount your code into `/app` and specify the command you want to run. When using kcov, you can also mount `/coverage` and output your coverage reports to that directory\n\n```sh\ndocker --rm -v$PWD:/app:ro shpy/shpy:1.0.0 zsh /app/tests/run_my_tests.sh\n#           ^-your project                 ^--------your command---------\n```\n\nThe following scripts and binaries are provided by this image\n\n| Name | Type | Location |\n| :--- | :--- | :------- |\n| shpy | script |  `/shpy/shpy` |\n| shpy-shunit2 | script | `/shpy/shpy-shunit2` |\n| shunit2 | script | `/usr/local/bin/shunit2` |\n| ash | binary | `/bin/sh` |\n| bash | binary | `/bin/bash` |\n| checkbashisms | binary | `/usr/bin/checkbashisms` |\n| dash | binary | `/usr/bin/dash` |\n| mksh | binary | `/bin/mksh` |\n| shellcheck | binary | `/usr/local/bin/shellcheck` |\n| zsh | binary | `/bin/zsh` |\n\n## Usage\n\nLet's try out shpy! If you don't want to install shpy locally you can run the official [Docker](https://www.docker.com) image like so:\n\n```sh\ndocker run -it --rm shpy/shpy:1.0.0\n```\n\nTo use shpy, the `SHPY_PATH` environment variable must be set as the path to shpy and the shpy script must be sourced. If you're using the Docker image, `SHPY_PATH` is already set and shpy is located at `/shpy/shpy`\n\n```sh\nSHPY_PATH=path/to/shpy\n. path/to/shpy\n```\n\nLet's create a spy for the `touch` command and call it!\n\n```sh\ncreateSpy touch\ntouch my-new-file\nls my-new-file # No such file or directory, touch wasn't actually called\n```\n\nThe call to `touch` was _stubbed out_ with a test dummy in place of the actual implementation. Spies record data about the calls made to them, allowing you to check the call count or call args\n\n```sh\ngetSpyCallCount touch # 1\nwasSpyCalledWith touch my-new-file # true\nwasSpyCalledWith touch my-old-file # false\ngetArgsForCall touch 1 # my-new-file\n```\n\nSpies can also simulate successful or unsuccessful calls, like so:\n\n```sh\ncreateSpy -o 'call me once, shame on you' -e '' -r 0 \\\n          -e 'call me twice, shame on me' -o '' -r 1 touch\ntouch my-new-file # outputs \"call me once, shame on you\" to stdout, returns true\ntouch my-new-file # outputs \"call me twice, shame on me\" to stderr, returns false\n```\n\nWhen developing tests for complex functions with long chained calls, source all of them and use spy with -u flag. The flag will unset declared function, so complex functions can be tested with a mix of spies and original functions. \n\n```sh\nsource my_script.sh # contains complex_function that calls file_check and directory_check\n\ncreateSpy -o 'spy test' -u file_check\ncomplex_function # complex_function calls file_check, which is mocked to output \"spy test\"\n                 # Without -u, createSpy would warn that file_check is already declared and wouldn't be mocked\n```\n\nWhen you're done playing with shpy, it's only polite to clean up after yourself\n\n```sh\ncleanupSpies\ntouch my-new-file\nls my-new-file # my-new-file, touch was actually called!\n```\n\nYour shell environment is back to normal, and you've got a new tool at your disposal! :mortar_board:\n\n## Contributing\n\nIf you'd like to help with shpy's development, or just gain a better understanding of the internals, check out the [contributor guidelines](https://github.com/codehearts/shpy/blob/master/CONTRIBUTING.md)\n\n## API Reference\n\nTo use shpy in your tests, set `SHPY_PATH` to the location of `shpy` and source the script:\n\n```\nSHPY_PATH=path/to/shpy\nexport SHPY_PATH\n\n. path/to/shpy\n```\n\nWhen using the Docker image, `SHPY_PATH` is preset as `/shpy/shpy` for convenience\n\t\nThe `SHPY_VERSION` environment variable is provided to get the current shpy version\n\nA summary of functions:\n\nFunction | Description\n---|---\n`createSpy name`                | Create a new spy, or reset an existing spy\n`createSpy -r status name`      | Sets the status code returned when the spy is invoked\u003cbr\u003eCan be passed multiple times to set a return value sequence\u003cbr\u003eOnce the sequence finishes, the last value is always returned\n`createSpy -o output name`      | Sets output sent to stdout when the spy is invoked\u003cbr\u003eCan be passed multiple times to set an output sequence\u003cbr\u003eOnce the sequence finishes, the last value is always output\u003cbr\u003eWhen used with `-e`, standard out is written to first\n`createSpy -e output name`      | Sets output sent to stderr when the spy is invoked\u003cbr\u003eCan be passed multiple times to set an error output sequence\u003cbr\u003eOnce the sequence finishes, the last value is always output\u003cbr\u003eWhen used with `-o`, standard out is written to first\n`createSpy -u name`             | A flag to unset declared function, so created spy can run instead otherwise declared function takes precedence\u003cbr\u003eFunction is not restored after test runs\n`createStub name`               | Alias for `createSpy`\n`getSpyCallCount name`          | Outputs the number of invocations of a spy\n`wasSpyCalledWith name [arg ...]` | Returns 0 if the current spy call under examination has the given args\n`getArgsForCall name call`      | Prints the arguments from a call to a spy (first call is 1)\u003cbr\u003eSingle-word arguments are always listed without quotes\u003cbr\u003eMulti-word arguments are always listed with double-quotes\n`examineNextSpyCall name`       | Examine the next spy invocation when calling `wasSpyCalledWith`\u003cbr\u003eThis causes `wasSpyCalledWith` to verify the second invocation, etc\n`cleanupSpies`                  | Clean up any metadata on disk or in the environment for a spy\n\n### shunit2 Integration\n\nTo use shpy asserts in your shunit2 tests, you must also source the\n`shpy-shunit2` script:\n\n\t. path/to/shpy\n\t. path/to/shpy-shunit2\n\t\nA summary of asserts:\n\nFunction                                              | Description\n------------------------------------------------------|------------------------------------------------------------------\n`assertCallCount [message] spy count`        | Assert the number of times the spy was invoked\n`assertCalledWith spy [arg ...]`             | Assert the arguments for the first invocation of the spy\u003cbr\u003eSubsequent calls will assert for the second invocation, etc\n`assertCalledWith_ message spy [arg ...]`    | Same as `assertCalledWith`, with a specific assertion message\n`assertCalledOnceWith spy [arg ...]`         | Assert the spy was called once and given the specified arguments\n`assertCalledOnceWith_ message spy [arg ...]`| Same as `assertCalledOnceWith`, with a specific assertion message\n`assertNeverCalled [message] spy`            | Assert the spy was never invoked\n\nUse the `tearDown` hook provided by shunit2 to remove all spies after each test\n\n```sh\ntearDown() {\n  cleanupSpies\n}\n```\n\n## A Word On Shell Portability\n\nshpy relies on [portable but more modern shell features](http://apenwarr.ca/log/?m=201102#28), such as the `local` keyword. To be clear, shpy does not use any [Bashisms](https://wiki.ubuntu.com/DashAsBinSh)\n\n[coverage-badge]:   https://codecov.io/gh/codehearts/shpy/branch/master/graph/badge.svg\n[coverage-link]:    https://codecov.io/gh/codehearts/shpy\n[license-badge]:    https://img.shields.io/badge/license-MIT-007EC7.svg\n[build-badge]:      https://img.shields.io/github/workflow/status/codehearts/shpy/Test/master\n[ash-build-badge]:  https://travis-matrix-badges.herokuapp.com/repos/codehearts/shpy/branches/master/1\n[bash-build-badge]: https://travis-matrix-badges.herokuapp.com/repos/codehearts/shpy/branches/master/2\n[dash-build-badge]: https://travis-matrix-badges.herokuapp.com/repos/codehearts/shpy/branches/master/3\n[mksh-build-badge]: https://travis-matrix-badges.herokuapp.com/repos/codehearts/shpy/branches/master/4\n[zsh-build-badge]:  https://travis-matrix-badges.herokuapp.com/repos/codehearts/shpy/branches/master/5\n[build-link]:       https://github.com/codehearts/shpy/actions?query=workflow%3ATest+branch%3Amaster\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodehearts%2Fshpy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodehearts%2Fshpy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodehearts%2Fshpy/lists"}