项目作者: commander-cli

项目描述 :
Test your command line interfaces on windows, linux and osx and nodes viá ssh and docker
高级语言: Go
项目地址: git://github.com/commander-cli/commander.git
创建时间: 2019-02-22T16:35:16Z
项目社区:https://github.com/commander-cli/commander

开源协议:MIT License

下载


Build Status
GoDoc
Go Report Card
Maintainability
Test Coverage
Github All Releases

Commander

Define language independent tests for your command line scripts and programs in simple yaml files.

  • It runs on windows, osx and linux
  • It can validate local machines, ssh hosts and docker containers
  • It is a self-contained binary - no need to install a heavy lib or language
  • It is easy and fast to write

For more information take a look at the quick start, the examples or the integration tests.

Table of contents

Installation

Any system with Go installed

Probably the easiest way to install commander is by using go get to download and install it in one simple command:

  1. go install github.com/commander-cli/commander/v2/cmd/commander@latest

This works on any OS, as long as go is installed. If go is not installed on your system, follow one of the methods below.

Linux & osx

Visit the release page to get the binary for you system.

  1. curl -L https://github.com/commander-cli/commander/releases/download/v2.5.0/commander-linux-amd64 -o commander
  2. chmod +x commander

Windows

  • Download the current release
  • Add the path to your path environment variable
  • Test it: commander --version

Quick start

A commander test suite consists of a config and tests root element. To start quickly you can use
the following examples.

  1. # You can even let commander add tests for you!
  2. $ ./commander add --stdout --file=/tmp/commander.yaml echo hello
  3. tests:
  4. echo hello:
  5. exit-code: 0
  6. stdout: hello
  7. written to /tmp/commander.yaml
  8. # ... and execute!
  9. $ ./commander test /tmp/commander.yaml
  10. Starting test file /tmp/commander.yaml...
  11. echo hello
  12. Duration: 0.002s
  13. Count: 1, Failed: 0

Complete YAML file

Here you can see an example with all features for a quick reference

  1. nodes:
  2. ssh-host1:
  3. type: ssh
  4. addr: 192.168.0.1:22
  5. user: root
  6. pass: pass
  7. ssh-host2:
  8. type: ssh
  9. addr: 192.168.0.1:22
  10. user: root
  11. identity-file: /home/user/id_rsa.pub
  12. docker-host1:
  13. type: docker
  14. image: alpine:2.4
  15. docker-host2:
  16. type: docker
  17. instance: alpine_instance_1
  18. config: # Config for all executed tests
  19. dir: /tmp #Set working directory
  20. env: # Environment variables
  21. KEY: global
  22. timeout: 50s # Define a timeout for a command under test
  23. retries: 2 # Define retries for each test
  24. nodes:
  25. - ssh-host1 # define default hosts
  26. tests:
  27. echo hello: # Define command as title
  28. stdout: hello # Default is to check if it contains the given characters
  29. exit-code: 0 # Assert exit-code
  30. it should skip:
  31. command: echo "I should be skipped"
  32. stdout: I should be skipped
  33. skip: true
  34. it should fail:
  35. command: invalid
  36. stderr:
  37. contains:
  38. - invalid # Assert only contain work
  39. not-contains:
  40. - not in there # Validate that a string does not occur in stdout
  41. exactly: "/bin/sh: 1: invalid: not found"
  42. line-count: 1 # Assert amount of lines
  43. lines: # Assert specific lines
  44. 1: "/bin/sh: 1: invalid: not found"
  45. json:
  46. object.attr: hello # Make assertions on json objects
  47. xml:
  48. "//book//auhtor": Steven King # Make assertions on xml documents
  49. file: correct-output.txt
  50. exit-code: 127
  51. skip: false
  52. it has configs:
  53. command: echo hello
  54. stdout:
  55. contains:
  56. - hello #See test "it should fail"
  57. exactly: hello
  58. line-count: 1
  59. config:
  60. inherit-env: true # You can inherit the parent shells env variables
  61. dir: /home/user # Overwrite working dir
  62. env:
  63. KEY: local # Overwrite env variable
  64. ANOTHER: yeah # Add another env variable
  65. timeout: 1s # Overwrite timeout
  66. retries: 5
  67. nodes: # overwrite default nodes
  68. - docker-host1
  69. - docker-host2
  70. exit-code: 0

Executing

  1. # Execute file commander.yaml in current directory
  2. $ ./commander test
  3. # Execute a specific suite
  4. $ ./commander test /tmp/test.yaml
  5. # Execute a single test
  6. $ ./commander test /tmp/test.yaml --filter "my test"
  7. # Execute suite from stdin
  8. $ cat /tmp/test.yaml | ./commander test -
  9. # Execute suite from url
  10. $ ./commander test https://your-url/commander_test.yaml
  11. # Execute suites within a test directory
  12. $ ./commander test --dir /tmp
  13. # Execute suites in a different working directory
  14. $ ./commander test --workdir /examples minimal_test.yaml

Adding tests

You can use the add argument if you want to commander to create your tests.

  1. # Add a test to the default commander.yaml
  2. $ ./commander add echo hello
  3. written to /tmp/commander.yaml
  4. # Write to a given file
  5. $ ./commander add --file=test.yaml echo hello
  6. written to test.yaml
  7. # Write to stdout and file
  8. $ ./commander add --stdout echo hello
  9. tests:
  10. echo hello:
  11. exit-code: 0
  12. stdout: hello
  13. written to /tmp/commander.yaml
  14. # Only to stdout
  15. $ ./commander add --stdout --no-file echo hello
  16. tests:
  17. echo hello:
  18. exit-code: 0
  19. stdout: hello

Documentation

Usage

  1. NAME:
  2. Commander - CLI app testing
  3. USAGE:
  4. commander [global options] command [command options] [arguments...]
  5. COMMANDS:
  6. test Execute the test suite
  7. add Automatically add a test to your test suite
  8. help, h Shows a list of commands or help for one command
  9. GLOBAL OPTIONS:
  10. --help, -h show help
  11. --version, -v print the version

Tests

Tests are defined in the tests root element. Every test consists of a command and an expected result,
i.e. an exit-code.

  1. tests: # root element
  2. echo test: # test case - can either be the command or a given title
  3. stdout: test
  4. exit-code: 0

A test is a map which configures the test.
The key (echo test in the example above) of the test can either be the command itself or the title of the test which will be displayed in the test execution.

If the same command is tested multiple times it is useful to set the title of the test manually and use the command property.
Further the title can be useful to describe tests better. See the commander test suite as an example.

  • name: title or command under test
  • type: map
  • default: {}

Examples:

  1. tests:
  2. echo test: # command and title will be the same
  3. stdout: test
  4. exit-code: 0
  5. my title: # custom title
  6. command: exit 1 # set command manually
  7. exit-code: 1

command

command is a string containing the command to be tested. Further the command property is automatically parsed from
the key if no command property was given.

  • name: command
  • type: string
  • default: can't be empty
  • notes: Will be parsed from the key if no command property was provided and used as the title too
  1. echo test: # use command as key and title
  2. exit-code: 0
  3. it should print hello world: # use a more descriptive title...
  4. command: echo hello world # ... and set the command in the property manually
  5. stdout: hello world
  6. exit-code: 0

config" class="reference-link">config

config sets configuration for the test. config can overwrite global configurations.

  • name: config
  • type: map
  • default: {}
  • notes:
    • for more information look at config
  1. echo test:
  2. config:
  3. timeout: 5s

exit-code

exit-code is an int type and compares the given code to the exit-code of the given command.

  • name: exit-code
  • type: int
  • default: 0
  1. exit 1: # will pass
  2. exit-code: 1
  3. exit 0: # will fail
  4. exit-code: 1

stdout

stdout and stderr allow to make assertions on the output of the command.
The type can either be a string or a map of different assertions.

If only a string is provided it will check if the given string is contained in the output.

  • name: stdout
  • type: string or map
  • default:
  • notes: stderr works the same way
  1. echo test:
  2. stdout: test # make a contains assertion
  3. echo hello world:
  4. stdout:
  5. line-count: 1 # assert the amount of lines and use stdout as a map
contains

contains is an array or string. It checks if a string is contained in the output.
It is the default if a string is directly assigned to stdout or stderr.

  • name: contains
  • type: string or array
  • default: []
  • notes: default assertion if directly assigned to stdout or stderr
  1. echo hello world:
  2. stdout: hello # Default is a contains assertion
  3. echo more output:
  4. stdout:
  5. contains:
  6. - more
  7. - output
exactly

exactly is a string type which matches the exact output.

  • name: exactly
  • type: string
  • default:
  1. echo test:
  2. stdout:
  3. exactly: test
json

json is a map type and allows to parse json documents with a given GJSON syntax to query for specific data.
The key represents the query, the value the expected value.

  • name: json
  • type: map
  • default: {}
  • notes: Syntax taken from GJSON
  1. cat some.json: # print json file to stdout
  2. name.last: Anderson # assert on name.last, see document below

some.json file:

  1. {
  2. "name": {"first": "Tom", "last": "Anderson"},
  3. "age":37,
  4. "children": ["Sara","Alex","Jack"],
  5. "fav.movie": "Deer Hunter",
  6. "friends": [
  7. {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
  8. {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
  9. {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
  10. ]
  11. }

More examples queries:

  1. "name.last" >> "Anderson"
  2. "age" >> 37
  3. "children" >> ["Sara","Alex","Jack"]
  4. "children.#" >> 3
  5. "children.1" >> "Alex"
  6. "child*.2" >> "Jack"
  7. "c?ildren.0" >> "Sara"
  8. "fav\.movie" >> "Deer Hunter"
  9. "friends.#.first" >> ["Dale","Roger","Jane"]
  10. "friends.1.last" >> "Craig"
lines

lines is a map which makes exact assertions on a given line by line number.

  • name: lines
  • type: map
  • default: {}
  • note: starts counting at 1 ;-)
  1. echo test\nline 2:
  2. stdout:
  3. lines:
  4. 2: line 2 # asserts only the second line
line-count

line-count asserts the amount of lines printed to the output. If set to 0 this property is ignored.

  • name: line-count
  • type: int
  • default: 0
  1. echo test\nline 2:
  2. stdout:
  3. line-count: 2
not-contains

not-contains is a array of elements which are not allowed to be contained in the output.
It is the inversion of contains.

  • name: not-contains
  • type: array
  • default: []
  1. echo hello:
  2. stdout:
  3. not-contains: bonjour # test passes because bonjour does not occur in the output
  4. echo bonjour:
  5. stdout:
  6. not-contains: bonjour # test fails because bonjour occurs in the output
xml

xml is a map which allows to query xml documents viá xpath queries.
Like the [json][#json] assertion this uses the key of the map as the query parameter to, the value is the expected value.

  • name: xml
  • type: map
  • default: {}
  • notes: Used library xmlquery
  1. cat some.xml:
  2. stdout:
  3. xml:
  4. //book//author: J. R. R. Tolkien

some.xml file:

  1. <book>
  2. <author>J. R. R. Tolkien</author>
  3. </book>
file

file is a file path, relative to the working directory that will have
its entire contents matched against the command output. Other than
reading from a file this works the same as exactly.

The example below will always pass.

  1. output should match file:
  2. command: cat output.txt
  3. stdout:
  4. file: output.txt

stderr

See stdout for more information.

  • name: stderr
  • type: string or map
  • default:
  • notes: is identical to stdout
  1. # >&2 echos directly to stderr
  2. ">&2 echo error":
  3. stderr: error
  4. exit-code: 0
  5. ">&2 echo more errors":
  6. stderr:
  7. line-count: 1

skip

skip is a boolean type, setting this field to true will skip the test case.

  • name: skip
  • type: bool
  • default: false
  1. echo test:
  2. stdout: test
  3. skip: true

Config" class="reference-link">Config

You can add configs which will be applied to all tests within a file or just for a specific test case, i.e.:

  1. config:
  2. dir: /home/root # Set working directory for all tests
  3. tests:
  4. echo hello:
  5. config: # Define test specific configs which overwrite global configs
  6. timeout: 5s
  7. exit-code: 0

You also have the option to define a separate config file that will
be applied globally to all test cases being ran using the --config flag.

The following will set the working directory for all tests that do not
explicitly set config.dir in the file config or the
test case config. In short, the lowest level config values takes precednce.

  1. ./commander test --config foo/bar/my-conifg.yaml foo/bar/test-suite.yaml
  1. # foo/bar/my-conifg.yaml`
  2. config:
  3. dir: /home/root # Set working directory for all tests
  1. # foo/bar/test-suite.yaml
  2. tests:
  3. echo hello:
  4. config: # Define test specific configs which overwrite global configs
  5. timeout: 5s
  6. exit-code: 0

dir

dir is a string which sets the current working directory for the command under test.
The test will fail if the given directory does not exist.

  • name: dir
  • type: string
  • default: current working dir
  1. dir: /home/root

env

env is a hash-map which is used to set custom env variables. The key represents the variable name and the value setting the value of the env variable.

  • name: env
  • type: hash-map
  • default: {}
  • notes:
    • read env variables with ${PATH}
    • overwrites inherited variables, see #inherit-env
  1. env:
  2. VAR_NAME: my value # Set custom env var
  3. CURRENT_USER: ${USER} # Set env var and read from current env

inherit-env

inherit-env is a boolean type which allows you to inherit all environment variables from your active shell.

  • name: inherit-env
  • type: bool
  • default: false
  • notes: If this config is set to true in the global configuration it will be applied for all tests and ignores local test configs.
  1. inherit-env: true

interval

interval is a string type and sets the interval between retries.

  • name: interval
  • type: string
  • default: 0ns
  • notes:
    • valid time units: ns, us, µs, ms, s, m, h
    • time string will be evaluated by golang’s time package, further reading time/#ParseDuration
  1. interval: 5s # Waits 5 seconds until the next try after a failed test is started

retries

retries is an int type and configures how often a test is allowed to fail until it will be marked as failed for the whole test run.

  • name: retries
  • type: int
  • default: 0
  • notes: interval can be defined between retry executions
  1. retries: 3 # Test will be executed 3 times or until it succeeds

timeout

timeout is a string type and sets the time a test is allowed to run.
The time is parsed from a duration string like 300ms.
If a tests exceeds the given timeout the test will fail.

  • name: timeout
  • type: string
  • default: no limit
  • notes:
    • valid time units: ns, us, µs, ms, s, m, h
    • time string will be evaluated by golang’s time package, further reading time/#ParseDuration
  1. timeout: 600s

Nodes

Commander has the option to execute tests against other hosts, i.e. via ssh.

Available node types are currently:

  • local, execute tests locally
  • ssh, execute tests viá ssh
  • docker, execute tests inside a docker container
  1. nodes: # define nodes in the node section
  2. ssh-host:
  3. type: ssh # define the type of the connection
  4. user: root # set the user which is used by the connection
  5. pass: password # set password for authentication
  6. addr: 192.168.0.100:2222 # target host address
  7. identity-file: ~/.ssh/id_rsa # auth with private key
  8. tests:
  9. echo hello:
  10. config:
  11. nodes: # define on which host the test should be executed
  12. - ssh-host
  13. stdout: hello
  14. exit-code: 0

You can identify on which node a test failed by inspecting the test output.
The [local] and [ssh-host] represent the node name on which the test were executed.

  1. [local] it should test ssh host
  2. [ssh-host] it should fail if env could not be set

local

The local node is the default execution and will be applied if nothing else was configured.
It is always pre-configured and available, i.e. if you want to execute tests on a node and locally.

  1. nodes:
  2. ssh-host:
  3. addr: 192.168.1.100
  4. user: ...
  5. tests:
  6. echo hello:
  7. config:
  8. nodes: # will be executed on local and ssh-host
  9. - ssh-host
  10. - local
  11. exit-code: 0

ssh

The ssh node type will execute tests against a configured node using ssh.

Limitations:

  • The inhereit-env config is disabled for ssh hosts, nevertheless it is possible to set env variables
  • Private registries are not supported at the moment
  1. nodes: # define nodes in the node section
  2. ssh-host:
  3. type: ssh # define the type of the connection
  4. user: root # set the user which is used by the connection
  5. pass: password # set password for authentication
  6. addr: 192.168.0.100:2222 # target host address
  7. identity-file: ~/.ssh/id_rsa # auth with private key
  8. tests:
  9. echo hello:
  10. config:
  11. nodes: # define on which host the test should be executed
  12. - ssh-host
  13. stdout: hello
  14. exit-code: 0

docker

The docker node type executes the given command inside a docker container.

Notes: If the default docker registry should be used prefix the container with the registry docker.io/library/

  1. nodes:
  2. docker-host:
  3. type: docker
  4. image: docker.io/library/alpine:3.11.3
  5. docker-exec-user: 1000 # define the owner of the executed command
  6. user: user # registry user
  7. pass: password # registry password, it is recommended to use env variables like $REGISTRY_PASS
  8. config:
  9. nodes:
  10. - docker-host
  11. tests:
  12. "id -u":
  13. stdout: "1001"

Development

See the documentation at development.md

Misc

Heavily inspired by goss.

Similar projects: