JackuB / shellspec

A full-featured BDD unit testing framework for bash, ksh, zsh, dash and all POSIX shells

Home Page:https://shellspec.info

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ShellSpec

ShellSpec is a full-featured BDD unit testing framework for dash, bash, ksh, zsh and all POSIX shells that provides first-class features such as code coverage, mocking, parallel execution, parameterized testing and more. It was developed as a dev/test tool for cross-platform shell scripts and shell script libraries. With lots of practical CLI features and simple yet powerful syntax, it provides you with a fun shell script test environment.

Travis CI Cirrus CI Circle CI GitHub Actions Status Docker Cloud Automated buildDocker Cloud Build Status
Kcov Coveralls Code Climate Codecov CodeFactor Grade GitHub top language GitHub release License

bash bosh busybox dash ksh mksh posh yash zsh


Thank you for your interest in ShellSpec. Please visit 🚩the official website to know the impressive features!

Let's have fun testing your shell scripts! (Try Online Demo on your browser).

demo

Coverage report

Latest Update.

See CHANGELOG.md

NOTE: This documentation contains unreleased features. Check them in the changelog.


Table of Contents

Requirements

Supported shells and platforms

Platform Test
Linux (Debian, Ubuntu, Fedora, CentOS, Alpine, Busybox, OpenWrt) Travis CI or Docker or manual
macOS (Default installed shells, Homebrew) Travis CI or manual
Windows (Git bash, msys2, cygwin, busybox-w32, WSL) Cirrus CI or manual
BSD (FreeBSD, OpenBSD, NetBSD) Cirrus CI or manual
Unix (Solaris, AIX) manual only

Tested version details

POSIX-compliant commands

ShellSpec uses shell built-in commands and only few basic POSIX-compliant commands to support widely environments (except kcov for optional code coverage).

Currently used external (not shell builtins) commands:

  • cat, date, env, ls, mkdir, od (or not POSIX hexdump), rm, sleep, sort, time
  • ps (use to auto-detect shells in environments that don't implement procfs)
  • ln, mv (use only when generating coverage report)
  • kill, printf (most shells except some are built-in)

Installation

Install the latest release version

curl -fsSL https://git.io/shellspec | sh

or

wget -O- https://git.io/shellspec | sh

NOTE: https://git.io/shellspec is redirected to https://github.com/shellspec/shellspec/raw/master/install.sh

Advanced installation / upgrade / uninstall

Automatic installation

curl -fsSL https://git.io/shellspec | sh -s -- --yes

Install the specified version

curl -fsSL https://git.io/shellspec | sh -s 0.19.1

Upgrade to the latest release version

curl -fsSL https://git.io/shellspec | sh -s -- --switch

Switch to the specified version

curl -fsSL https://git.io/shellspec | sh -s 0.18.0 --switch

How to uninstall

  1. Delete the ShellSpec executable file [default: $HOME/.local/bin/shellspec].
  2. Delete the ShellSpec installation directory [default: $HOME/.local/lib/shellspec].

Other usage

$ curl -fsSL https://git.io/shellspec | sh -s -- --help
Usage: [sudo] ./install.sh [VERSION] [OPTIONS...]
  or : wget -O- https://git.io/shellspec | [sudo] sh
  or : wget -O- https://git.io/shellspec | [sudo] sh -s -- [OPTIONS...]
  or : wget -O- https://git.io/shellspec | [sudo] sh -s VERSION [OPTIONS...]
  or : curl -fsSL https://git.io/shellspec | [sudo] sh
  or : curl -fsSL https://git.io/shellspec | [sudo] sh -s -- [OPTIONS...]
  or : curl -fsSL https://git.io/shellspec | [sudo] sh -s VERSION [OPTIONS...]

VERSION:
  Specify install version and method

  e.g
    1.0.0           Install 1.0.0 from git
    master          Install master from git
    1.0.0.tar.gz    Install 1.0.0 from tar.gz archive
    .               Install from local directory

OPTIONS:
  -p, --prefix PREFIX   Specify prefix                 [default: $HOME/.local]
  -b, --bin BIN         Specify bin directory          [default: <PREFIX>/bin]
  -d, --dir DIR         Specify installation directory [default: <PREFIX>/lib/shellspec]
  -s, --switch          Switch version (requires installed via git)
  -l, --list            List available versions (tags)
      --pre             Include pre-release
      --fetch FETCH     Force command to use when install from archive (curl or wget)
  -y, --yes             Automatic yes to prompts
  -h, --help            You're looking at it
Package manager (Arch Linux / Homebrew / Linuxbrew / basher / bpkg)

Arch Linux

Installation on Arch Linux from the AUR ShellSpec package using aura:

# Install the latest stable version
$ aura -A shellspec

Homebrew / Linuxbrew

# Install the latest stable version
$ brew tap shellspec/shellspec
$ brew install shellspec

basher

Installation with basher

The officially supported version is ShellSpec 0.19.1 and later.

# Install from master branch
$ basher install shellspec/shellspec

# To specify a version (example: 0.19.1)
$ basher install shellspec/shellspec@0.19.1

bpkg

Installation with bpkg

The officially supported version is ShellSpec 0.19.1 and later.

# Install from master branch
$ bpkg install shellspec/shellspec

# To specify a version (example: 0.19.1)
$ bpkg install shellspec/shellspec@0.19.1
Other methods (archive / make / manual)

Archive

See Releases page if you want to download distribution archive.

Make

How to install.

Install to /usr/local/bin and /usr/local/lib

sudo make install

Install to $HOME/bin and $HOME/lib

make install PREFIX=$HOME

How to uninstall.

sudo make uninstall
make uninstall PREFIX=$HOME

Manual installation

Just get ShellSpec and create a symlink in your executable PATH!

From git

$ cd /SOME/WHERE/TO/INSTALL
$ git clone https://github.com/shellspec/shellspec.git
$ ln -s /SOME/WHERE/TO/INSTALL/shellspec/shellspec /EXECUTABLE/PATH/
# (e.g. /EXECUTABLE/PATH/ = /usr/local/bin/, $HOME/bin/)

From tar.gz

$ cd /SOME/WHERE/TO/INSTALL
$ wget https://github.com/shellspec/shellspec/archive/{VERSION}.tar.gz
$ tar xzvf shellspec-{VERSION}.tar.gz

$ ln -s /SOME/WHERE/TO/INSTALL/shellspec-{VERSION}/shellspec /EXECUTABLE/PATH/
# (e.g. /EXECUTABLE/PATH/ = /usr/local/bin/, $HOME/bin/)

If you can't create symlink (like default of Git for Windows), create the shellspec file.

$ cat<<'HERE'>/EXECUTABLE/PATH/shellspec
#!/bin/sh
exec /SOME/WHERE/TO/INSTALL/shellspec/shellspec "$@"
HERE
$ chmod +x /EXECUTABLE/PATH/shellspec

Tutorial

Just create your project directory and run shellspec --init to setup to your project

# Create your project directory, for example "hello".
$ mkdir hello
$ cd hello

# Initialize
$ shellspec --init
  create   .shellspec
  create   spec/spec_helper.sh
  create   spec/hello_spec.sh # sample

# Write your first specfile (of course you can use your favorite editor)
$ cat<<'HERE'>spec/hello_spec.sh
Describe 'hello.sh'
  Include lib/hello.sh
  It 'says hello'
    When call hello ShellSpec
    The output should equal 'Hello ShellSpec!'
  End
End
HERE

# Create lib/hello.sh
$ mkdir lib
$ touch lib/hello.sh

# It will fail because the hello function is not implemented.
$ shellspec

# Write hello function
$ cat<<'HERE'>lib/hello.sh
hello() {
  echo "Hello ${1}!"
}
HERE

# It will success!
$ shellspec

ShellSpec CLI

See more info: ShellSpec CLI

NOTE: ShellSpec CLI ignores shebang except in some cases and runs specfiles with the shell running shellspec (normally /bin/sh). For example, if you want to run specfiles in bash, specify the -s (--shell) option or add the option to .shellspec file.

$ shellspec -h
Usage: shellspec [options...] [files or directories...]

  Using + instead of - for short options causes reverses the meaning

    -s, --shell SHELL               Specify a path of shell [default: "auto" (the shell running shellspec)]
        --path PATH                 Set PATH environment variable at startup
        --[no-]sandbox              Force the use of the mock instead of the actual command
        --sandbox-path SANDBOX-PATH Make PATH the sandbox path instead of empty (default: empty)
        --require MODULE            Require a MODULE (shell script file)
    -e, --env NAME=VALUE            Set environment variable
        --env-from ENV-SCRIPT       Set environment variable from shell script file
    -w, --[no-]warning-as-failure   Treat warning as failure [default: enabled]
        --[no-]fail-fast[=COUNT]    Abort the run after first (or COUNT) of failures [default: disabled]
        --[no-]fail-no-examples     Fail if no examples found [default: disabled]
        --[no-]fail-low-coverage    Fail on low coverage [default: disabled]
    -p, --[no-]profile              Enable profiling and list the slowest examples [default: disabled]
        --profile-limit N           List the top N slowest examples [default: 10]
        --[no-]boost                Increase the CPU frequency to boost up testing speed [default: disabled]
        --log-file LOGFILE          Log file for %logger directive and trace [default: /dev/tty]
        --keep-tempdir              Do not cleanup temporary directory [default: disabled]

  **** Execution ****

    -q, --[no-]quick                Run not-passed examples if it exists, otherwise run all [default: disabled]
    -r, --repair, --only-failures   Run failure examples only (Depends on quick mode)
    -n, --next,   --next-failure    Run failure examples and abort on first failure (Depends on quick mode)
    -j, --jobs JOBS                 Number of parallel jobs to run [default: 0 (disabled)]
        --random TYPE[:SEED]        Run examples by the specified random type | <[none]> [specfiles] [examples]
    -x, --xtrace                    Run examples with trace output of evaluation enabled [default: disabled]
    -X, --xtrace-only               Run examples with trace output only enabled [default: disabled]
        --dry-run                   Print the formatter output without running any examples [default: disabled]

  **** Output ****

        --[no-]banner               Show banner if exist 'spec/banner' [default: enabled]
    -f, --format FORMATTER          Choose a formatter for display | <[p]> [d] [t] [j] [f] [null] [debug]
    -o, --output GENERATOR          Choose a generator(s) to generate a report file(s) [default: none]
        --[no-]color                Enable or disable color [default: enabled if the output is a TTY]
        --skip-message VERBOSITY    Mute skip message | <[verbose]> [moderate] [quiet]
        --pending-message VERBOSITY Mute pending message | <[verbose]> [quiet]
        --quiet                     Equivalent of --skip-message quiet --pending-message quiet
        --(show|hide)-deprecations  Show or hide deprecations details [default: show]

  **** Ranges / Filters / Focus ****

    You can run selected examples by specified the line numbers or ids

      shellspec path/to/a_spec.sh:10    # Run the groups or examples that includes lines 10
      shellspec path/to/a_spec.sh:@1-5  # Run the 5th groups/examples defined in the 1st group
      shellspec a_spec.sh:10:@1:20:@2   # You can mixing multiple line numbers and ids with join by ':'

    -F, --focus                     Run focused groups / examples only
    -P, --pattern PATTERN           Load files matching pattern [default: "*_spec.sh"]
    -E, --example PATTERN           Run examples whose names include PATTERN
    -T, --tag TAG[:VALUE]           Run examples with the specified TAG
    -D, --default-path PATH         Set the default path where looks for examples [defualt: "spec"]

  **** Coverage ****

        --[no-]kcov                 Enable coverage using kcov [default: disabled]
        --kcov-path PATH            Specify kcov path [default: kcov]
        --kcov-options OPTIONS      Additional Kcov options (coverage limits, coveralls id, etc)

  **** Utility ****

        --init [TEMPLATE...]        Initialize your project with ShellSpec | [git] [hg] [svn]
        --gen-bin [@COMMAND...]     Generate test support commands in spec/support/bin
        --count                     Count the number of specfiles and examples
        --list LIST                 List the specfiles/examples | [specfiles] [examples(:id|:lineno)]
        --syntax, --syntax-check    Syntax check of the specfiles without running any examples
        --translate                 Output translated specfile
        --docker DOCKER-IMAGE       Run tests in specified docker image (EXPERIMENTAL)
        --task [TASK]               Run the TASK or Show the task list if TASK is not specified
    -v, --version                   Display the version
    -h, --help                      -h: short help, --help: long help

Project directory structure

See more info: Directory structure

Typical project directory structure

Project directory
├─ .shellspec                 [Required]
├─ .shellspec-local           [Optional, Ignore from VCS]
├─ .shellspec-quick.log       [Optional, Ignore from VCS]
├─ report/                    [Optional, Ignore from VCS]
├─ coverage/                  [Optional, Ignore from VCS]
│
├─ bin/
│   ├─ your_script1.sh
│              :
├─ lib/
│   ├─ your_library1.sh
│              :
├─ libexec/
│   ├─ project-your_script1.sh
│              :
├─ spec/
│   ├─ banner                 [Optional]
│   ├─ spec_helper.sh         [Required]
│   ├─ support/               [Optional]
│   │
│   ├─ bin/
│   │   ├─ your_script1_spec.sh
│   │             :
│   ├─ lib/
│   │   ├─ your_library1_spec.sh
│   │             :
│   ├─ libexec/
│   │   ├─ project-your_script1_spec.sh
│                  :

DSL syntax

The best place to learn how to write a specfile is the sample/spec directory. You should take a look at it ! (Those samples include failure examples on purpose.)

Describe 'lib.sh' # example group
  Describe 'bc command'
    add() { echo "$1 + $2" | bc; }

    It 'performs addition' # example
      When call add 2 3 # evaluation
      The output should eq 5  # expectation
    End
  End
End

Basic structure

Describe, Context, ExampleGroup - example group

ExampleGroup is a block for grouping example groups or examples. Describe and Context are alias for ExampleGroup. It can be nested and they can contain example groups or examples.

Describe 'is example group'
  Describe 'is nestable'
    ...
  End

  Context 'is used to make easier to understand depending on the context'
    ...
  End
End
xDescribe, xContext, xExampleGroup - skipped example group

xDescribe, xContext, xExampleGroup are skipped example group block. Execution of example contained in these is skipped.

Describe 'is example group'
  xDescribe 'is skipped example group'
    ...
  End
End
fDescribe, fContext, fExampleGroup - focused example group

fDescribe, fContext, fExampleGroup are focused example group block. Only the examples included in these will be executed when the --focus option is specified.

Describe 'is example group'
  fDescribe 'is focues example group'
    ...
  End
End

It, Specify, Example - example

Example is a block for writing evaluation and expectations. It and Specify are alias for Example.

An example is composed by up to one evaluation and multiple expectations.

add() { echo "$1 + $2" | bc; }

It 'performs addition'          # example
  When call add 2 3             # evaluation
  The output should eq 5        # expectation
  The status should be success  # another expectation
End
xIt, xSpecify, xExample - skipped example

xIt, xSpecify, xExample are skipped example block. Execution of example is skipped.

xIt 'is skipped example'
  ...
End
fIt, fSpecify, fExample - focused example

fIt, fSpecify, fExample are focused example block. Only these examples will be executed when the --focus option is specified.

fIt 'is focused example'
  ...
End
Todo - one liner pending example

Todo is a pending one-liner example, the same as the empty example.

Todo 'will be used later when write a test'

It 'is empty example, the same as Todo'
End

When - evaluation

Evaluation executes shell function or command for verification. Only one evaluation can be defined for each example and also can be omitted.

See more details of Evaluation

call - call a shell function

It call a function without subshell. Practically, it can also run commands.

When call add 1 2 # call `add` shell function with two arguments.
run - run a command

It runs a command within subshell. Practically, it can also call shell function. The command does not have to be a shell script.

NOTE: This is not supporting coverage measurement.

When run touch /tmp/foo # run `touch` command.
run command - run a external command

It runs a command, respecting shebang. It can not call shell function. The command does not have to be a shell script.

NOTE: This is not supporting coverage measurement.

When run command touch /tmp/foo # run `touch` command.
run script - run a shell script

It runs a shell script, ignoring shebang. The script have to be a shell script. It will be executed in another instance of the same shell as the current shell.

When run script my.sh # run `my.sh` script.
run source - run a script by . (source) command

It source a shell script, ignoring shebang. The script have to be a shell script. It similar to run script, but with some differences. Unlike run script, function-based mock is available.

When run source my.sh # source `my.sh` script.

The - expectation

Expectation begin with The, which does the verification. The basic syntax is as follows:

The output should equal 4

Use should not for the opposite verification.

The output should not equal 4
Subjects

The subject is the target of verification.

The output should equal 4
      |
      +-- subject

There are output (stdout), error (stdout), status, variable, path, etc.

Please refer to the Subjects for more details.

Modifiers

The modifier is modified the verification target.

The line 2 of output should equal 4
      |
      +-- modifier

The modifier is chainable.

The word 1 of line 2 of output should equal 4

If the modifier argument is a number, you can use ordinal numbers instead of a number.

The first word of second line of output should equal 4

There are line, word, length, contents, result, etc. The result modifier is useful for making the result of a user-defined function the subject.

Please refer to the Modifiers for more details.

Matchers

The matcher is the verification.

The output should equal 4
                   |
                   +-- matcher

There are many matchers such as string matcher, status matcher, variable matchers and stat matchers. The satisfy matcher is useful for verification with user-defined function.

Please refer to the Matchers for more details.

Language chains

ShellSpec supports language chains like chai.js. It only improves readability, does not affect the expectation: a, an, as, the.

The following two sentences have the same meaning:

The first word of second line of output should valid number

The first word of the second line of output should valid as a number

Assert - expectation for custom assertion

The Assert is yet another expectation to verify with a user-defined function. It is designed for verification of side effects, not result of evaluation.

still_alive() {
  ping -c1 "$1" >/dev/null
}

Describe "example.com"
  It "responses"
    Assert still_alive "example.com"
  End
End

Skip, Pending - skip and pending example

Use Skip to skip executing the example. If you want to skip only in some cases, use a conditional skip Skip if.

Describe 'Skip'
  Skip "not exists bc"

  It 'is always skip'
    ...
  End
End

Describe 'Conditional skip'
  not_exists_bc() { ! type bc >/dev/null 2>&1; }
  Skip if "not exists bc" not_exists_bc

  add() { echo "$1 + $2" | bc; }

  It 'performs addition'
    When call add 2 3
    The output should eq 5
  End
End

Pending is similar to Skip, but the test passes if the validation fails, and the test fails if the validation succeeds. This is useful if you want to specify that you will implement it later.

Describe 'Pending'
  Pending "not implemented"

  hello() { :; }

  It 'will success when test fails'
    When call hello world
    The output should "Hello world"
  End
End
Temporary skip and pending

The (non-temporary) skip and pending is for long term skip and pending. It need time to resolve and it may commit to a version control system.

The temporary skip and pending is for short term skip and pending. Used during the current work, do not commit to a version control system.

The skip and pending without message is temporary skip and pending.

Skip "some reason" # Skip with message is non-temporary skip
Skip if "reason" condition # Skip with condition is also non-temporary skip
Skip # temporary skip (this is comment but will be displayed in the report)

Hooks

Before, After - example hook

You can specify commands to be executed before / after each example by Before and After

NOTE: Do not assert in After hook. it is a place to clean up.

Describe 'example hook'
  setup() { :; }
  cleanup() { :; }
  Before 'setup'
  After 'cleanup'

  It 'is called before and after each example'
    ...
  End

  It 'is called before and after each example'
    ...
  End
End

BeforeAll, AfterAll - example group hook

You can specify commands to be executed before / after all examples by BeforeAll and AfterAll

Describe 'example all hook'
  setup() { :; }
  cleanup() { :; }
  BeforeAll 'setup'
  AfterAll 'cleanup'

  It 'is called before/after all example'
    ...
  End

  It 'is called before/after all example'
    ...
  End
End

BeforeCall, AfterCall - call evaluation hook

You can specify commands to be executed before / after call evaluation by BeforeCall and AfterCall

NOTE: These hooks were originally created to test ShellSpec itself. Please use the Before / After hooks whenever possible.

Describe 'call evaluation hook'
  setup() { :; }
  cleanup() { :; }
  BeforeCall 'setup'
  AfterCall 'cleanup'

  It 'is called before/after call evaluation'
    When call hello world
    ...
  End
End

BeforeRun, AfterRun - run evaluation hook

You can specify commands to be executed before / after run evaluation (run, run command, run script and run source) by BeforeRun and AfterRun

These hooks are executed in the same subshell as the "run evaluation". Therefore, you can access the variables after executing the evaluation.

NOTE: These hooks were originally created to test ShellSpec itself. Please use the Before / After hooks whenever possible.

Describe 'run evaluation hook'
  setup() { :; }
  cleanup() { :; }
  BeforeRun 'setup'
  AfterRun 'cleanup'

  It 'is called before/after run evaluation'
    When run hello world
    ...
  End
End

Helpers

Dump - dump stdout, stderr and status

Dump stdout, stderr and status of the evaluation. It is useful for debugging.

When call echo hello world
Dump # stdout, stderr and status

Include - include a script file

Include the shell script to test.

Describe 'lib.sh'
  Include lib.sh # hello function defined

  Describe 'hello()'
    It 'says hello'
      When call hello ShellSpec
      The output should equal 'Hello ShellSpec!'
    End
  End
End

Set - set shell option

Set shell option before executing each example. The shell option name is the long name of set or the name of shopt:

NOTE: Use Set instead of the set command because the set command may not work as expected in some shells.

Describe 'Set helper'
  Set 'errexit:off' 'noglob:on'

  It 'sets shell options before executiong the example'
    When call foo
  End
End

Path, File, Dir - path alias

Path is used to define a short pathname alias. File and Dir are alias for Path.

Describe 'Path helper'
  Path hosts-file="/etc/hosts"

  It 'defines short alias for long path'
    The path hosts-file should be exists
  End
End

Data - input data for evaluation

You can use the Data Helper which inputs data from stdin for evaluation. The input data is specified after #| in the Data or Data:expand block.

Describe 'Data helper'
  It 'provides with Data helper block style'
    Data # Use Data:expand instead if you want expand variables.
      #|item1 123
      #|item2 456
      #|item3 789
    End
    When call awk '{total+=$2} END{print total}'
    The output should eq 1368
  End
End

You can also use a file, function, or string as data sources.

See more details of Data

Parameters - parameterized example

Parameterized test (aka Data Driven Test) is used to run the same test with different parameters. Parameters defines its parameters.

Describe 'example'
  Parameters
    "#1" 1 2 3
    "#2" 1 2 3
  End

  Example "example $1"
    When call echo "$(($2 + $3))"
    The output should eq "$4"
  End
End

In addition to the default Parameters, three styles are supported: Parameters:value, Parameters:matrix and Parameters:dynamic.

See more details of Parameters

NOTE: You can also cooperate the Parameters and Data:expand helpers.

Mock - create a command-based mock

See Command-based mock

Intercept - create an intercept point

See Intercept

Directives

Directives are instructions that can be used in embedded shell scripts. It is used to solve small problems of shell scripts in testing.

This is like a shell function, but not a shell function. Therefore, the supported grammar is limited and can only be used at the beginning of a function definition or at the beginning of a line.

foo() { %puts "foo"; } # supported

bar() {
  %puts "bar" # supported
}

baz() {
  any command; %puts "baz" # not supported
}

%const (%) - constant definition

%const (% is short hand) directive defines a constant value. The characters which can be used for variable names are uppercase letters [A-Z], digits [0-9] and underscore _ only. It can not be defined inside an example group nor an example.

The value is evaluated during the specfile translation process. So you can access ShellSpec variables, but you can not access variable or function in the specfile.

This feature assumed use with conditional skip. The conditional skip may runs outside of the examples. As a result, sometime you may need variables defined outside of the examples.

%text - embedded text

You can use the %text directive instead of an hard-to-use heredoc with indented code. The input data is specified after #|.

Describe '%text directive'
  It 'outputs texts'
    output() {
      echo "start" # you can write code here
      %text
      #|aaa
      #|bbb
      #|ccc
      echo "end" # you can write code here
    }

    result() { %text
      #|start
      #|aaa
      #|bbb
      #|ccc
      #|end
    }

    When call output
    The output should eq "$(result)"
    The line 3 of output should eq 'bbb'
  End
End

%puts (%-), %putsn (%=) - output a string (with newline)

%puts (put string) and %putsn (put string with newline) can be used instead of (not portable) echo. Unlike echo, it does not interpret escape sequences regardless of the shell. %- is an alias of %puts, %= is an alias of %putsn.

%printf - alias for printf

This is same as printf, But it can be used in sandbox mode because the path has been resolved.

%sleep - alias for sleep

This is same as sleep, But it can be used in sandbox mode because the path has been resolved.

%preserve - preserve variables

Use %preserve directive to preserve the variables in subshells and external shell script.

In the following cases, %preserve is required because variables are not preserved.

  • When run evaluation - It runs in a subshell.
  • Command-based mock (Mock) - It is an external shell script.
  • Function-based Mock called by command substitution
Describe '%preserve directive'
  It 'preserves variables'
    func() { foo=1; bar=2; baz=3; }
    preserve() { %preserve bar baz:BAZ; }
    AfterRun preserve

    When run func
    The variable foo should eq 1 # This will be failure
    The variable bar should eq 2 # This will be success
    The variable BAZ should eq 3 # Preserved to different variable (baz:BAZ)
  End
End

%logger - debug output

Output log messages to the log file (default: /dev/tty) for debugging.

%data - define parameter

See Parameters.

Mocking

There are two ways to create a mock, function-based mock and command-based mock. The function-based mock is usually recommended for performance reasons. Both can be overwritten with an internal block and will be restored when the block ends.

Function-based mock

The function-based mock is simply (re)defined with shell function.

Describe 'function-based mock'
  get_next_day() { echo $(($(date +%s) + 86400)); }

  date() {
    echo 1546268400
  }

  It 'calls the date function'
    When call get_next_day
    The stdout should eq 1546354800
  End
End

Command-based mock

The command-based mock is create a temporary mock shell script and run as external command. To accomplish this, a directory for mock commands is included at the beginning of PATH.

This is slow, but there are some advantages over function-based.

  • You can use invalid characters as the shell function name.
    • e.g docker-compose (It can be defined with bash etc., but invalid as POSIX.)
  • You can use the mock command from an external shell script.

A command-based mock creates an external shell script with the contents of a Mock block. Therefore, there are some restrictions.

  • You cannot call shell functions outside the Mock block.
    • Only bash can export and call shell functions with export -f.
  • To reference a variable outside the Mock block, that variable must be exported.
  • %preserve directive is required to return a variable from a Mock block.
Describe 'command-based mock'
  get_next_day() { echo $(($(date +%s) + 86400)); }

  Mock date
    echo 1546268400
  End

  It 'runs the mocked date command'
    When call get_next_day
    The stdout should eq 1546354800
  End
End

Support commands

Execute the actual command within a mock function

Support commands are helper commands that can be used in the specfile. For example, it can be used in a mock function to execute the actual command. It is recommended that the support command name be the actual command name prefixed with @.

Describe "Support commands sample"
  touch() {
    @touch "$@" # @touch executes actual touch command
    echo "$1 was touched"
  }

  It "touch a file"
    When run touch "file"
    The output should eq "file was touched"
    The file "file" should be exist
  End
End

Support commands are generate to the spec/support/bin directory by --gen-bin option. For example, run shellspec --gen-bin @touch to generate the @touch command.

This is main purpose but support commands is just shell script, so you can also be used for other purposes. You can freely edit the support command script.

Make mock not mandatory in sandbox mode

The sandbox mode is force the use of the mock. However, you may not want to require mocks in some commands. For example, printf is a built-in command in many shells and does not require a mock, but some shells require a mock in sandbox mode because it is an external command.

To allow printf to be called without mocking in such cases, create a support command named printf (shellspec --gen-bin printf).

Resolve command incompatibilities

Some commands have different options between BSD and GNU. If you handle the difference in the specfile, the test will be hard to read. You can solve it with the support command.

#!/bin/sh -e
# Command name: @sed
. "$SHELLSPEC_SUPPORT_BIN"
case $OSTYPE in
  *darwin*) invoke gsed "$@" ;;
  *) invoke sed "$@" ;;
esac

Testing a single file script

Shell scripts are often made up of a single file. ShellSpec provides two ways of testing a single shell script.

Sourced Return

This is a method for testing functions defined in shell scripts. Loading a script with Include defines a __SOURCED__ variable available in the sourced script. If the __SOURCED__ variable is defined, return in your shell script process.

#!/bin/sh
# hello.sh

hello() { echo "Hello $1"; }

${__SOURCED__:+return}

hello "$1"
Describe "sample"
  Include "./hello.sh"
  Example "hello test"
    When call hello world
    The output should eq "Hello world"
  End
End

Intercept

This is a method to mock functions and commands when executing shell scripts. By placing intercept points in your script, you can call the hooks defined in specfile.

#!/bin/sh
# today.sh

test || __() { :; }

__ begin __

date +"%A, %B %d, %Y"
Describe "sample"
  Intercept begin
  __begin__() {
    date() {
      export LANG=C
      command date "$@" --date="2019-07-19"
    }
  }
  Example "today test"
    When run source ./today.sh
    The output should eq "Friday, July 19, 2019"
  End
End

Self-executable specfile

Normally, you use shellspec to run a specfile. If you want to run a specfile directly, use shebang below and give execute permission.

#!/usr/bin/env shellspec

If you want to use #!/bin/sh as shebang, by adding eval "$(shellspec -)" to the top of the specfile.

#!/bin/sh
# 'test.sh' with executable permission

eval "$(shellspec -)"

Describe "bc command"
  bc() { echo "$@" | command bc; }

  It "performs addition"
    When call bc "2+3"
    The output should eq 5
  End
End
# You can run 'test.sh' directly
$ ./test.sh
Running: /bin/sh [sh]
.

Finished in 0.12 seconds (user 0.00 seconds, sys 0.10 seconds)
1 example, 0 failures

# Also you can run via shellspec
$ shellspec test.sh

Use with Docker

You can run ShellSpec without installation by using Docker. ShellSpec and specfiles run in a Docker container.

See How to use ShellSpec with Docker.

Extension

Custom subject, modifier and matcher

You can create custom subject, custom modifier and custom matcher.

See sample/spec/support/custom_matcher.sh for custom matcher.

NOTE: If you want to verify using shell function, You can use result modifier or satisfy matcher. You don't necessarily have to create a custom matcher, etc.

For developers

If you want to know ShellSpec architecture and self test, see CONTRIBUTING.md

About specfile translation process

The specfile is a valid shell script, but a translation process is performed to implement the scope, line number etc. Each example group block and example block is translated to commands in a subshell. Therefore changes inside those blocks do not affect the outside of the block. In other words it realizes local variables and local functions in the specfile. This is very useful for describing a structured spec. If you are interested in how to translate, use the --translate option.

Subproject

  • ShellMetrics - Cyclomatic Complexity Analyzer for shell scripts
  • ShellBench - A benchmark utility for POSIX shell comparison

About

A full-featured BDD unit testing framework for bash, ksh, zsh, dash and all POSIX shells

https://shellspec.info

License:MIT License


Languages

Language:Shell 95.1%Language:Dockerfile 4.5%Language:Makefile 0.2%Language:C 0.2%