├── cmd # Contains the composition root and files which will be compiled to binaries.
├── docs # Documentation for commander
├── examples # Examples of how to use commander with to give you a little bit inspiration.
├── hack # Just a directory for testing stuff and shitty dev scripts.
├── integration # Integration tests for all platforms, all written as test suites for commander.
├── pkg # All packages written in GoLang which are used to compose this tool.
├── release # All binaries which are created on `make release` are located here. Directory is in `.gitignore`.
└── vendor # Third party dependencies added by `go mod`.
package | description | path |
---|---|---|
main | The composition root of the commander, initializes the urfave/cli framework. |
/cmd/commander |
app | Contains all commands for commander, i.e. add or test. Will only be used by the cmd package located under /cmd |
/pkg/app |
matcher | Implements the matching logic for the different assertions types in the suite like equals, contains, jsonor xml`. |
/pkg/matcher |
output | Output is a package which allows to add different output types. For example it could be possible to add a new output format in json or for a health check. |
/pkg/output |
runtime | Runtime controls the execution of the test suite. It will be initialized by the main package and will be a given a Suite which contains all tests to be executed. The runtime also contains all executors which are different types of ways to execute a test, i.e. on a local machine or a node viá ssh. |
/pkg/runtime |
suite | Suite is responsible for parsing the defined formats of different test suites. At the moment only yaml is supported but it would be possible to add support for a custom DSL or formats like toml and json . The suite.Suite struct will be used by the runtime.Runtime for executing the tests. |
/pkg/suite |
# Initialise dev environment
$ make init
# Build the project binary
$ make build
# Unit tests
$ make test
# Coverage
$ make test-coverage
# Coverage with more complex tests like ssh execution
$ make test-coverage-all-dockerized
# Integration tests for linux and macos
$ make integration-unix
# Integration on linux
$ make integration-linux
# Integration windows
$ make integration-windows
# Add depdencies to vendor
$ make deps
COMMANDER_TEST_ALL
will enable all tests which are depending on external systems like docker or databases.
Enables ssh tests in unit test suite and sets the credentials for the target host.
COMMANDER_SSH_TEST
must be set to 1
to enable ssh tests.
Note: I am aware that unit tests should not test external systems nor libraries, but in favour of simplicity and laziness I created simple tests inside the directory tree.
export COMMANDER_TEST_ALL=1
export COMMANDER_TEST_SSH=1
export COMMANDER_TEST_SSH_HOST=localhost:2222
export COMMANDER_TEST_SSH_PASS=pass
export COMMANDER_TEST_SSH_USER=root
export COMMANDER_TEST_SSH_IDENTITY_FILE=integration/containers/ssh/.ssh/id_rsa
It is a little bit annoying to add fields because it will be converted multiple times to keep the
suite format abstracted from the runtime package.
The idea behind this it to add support for other formats like json
, toml
or maybe a custom DSL.
Definition of done:
- Add a property
message
which always display a message while executing a testtests: echo hello: exit-code: 0 config: message: this is a very special test
- Support global configurations
- Create a simple test case
1. Extend the conversion struct suite.YAMLTestConfigConf
in yaml_suite.go with the Message
property.
The structs types with a Conf
suffix represent the configuration type, the YAML
prefix the format of the suite.
The naming makes it a little bit clearer in the code which type is used, i.e. a runtime.CommandUnderTest
or a suite.YAMLTestConfigConf
.
```go
Message string `yaml:"message,omitempty"`
```
2. Add the Message
property as a string
to the runtime.CommandUnderTest
struct
The runtime.CommandUnderTest
struct in runtime.go will be used by the runtime to
create the command with all its configs like env
variables and is used for the test execution.
3. Add a simple test case
Open yaml_suite_test.go and look for an existing test to add this property or create a new one.
I recommend to create a simple test case before adding the properties because it is easier to debug and test.
In our example we could extend the TestYAMLSuite_ShouldPreferLocalTestConfigs
test but will add a new TestYAMLSuite_Message
test for the simplicity.
func TestYamlSuite_Message(t *testing.T) {
yaml := []byte(`
tests:
echo hello:
exit-code: 0
config:
message: "This is a very special test"
`)
got := ParseYAML(yaml)
assert.Equal(t, "This is a very special test", got.TestCases[0].Command.Message)
}
Run the unit tests with make test
. It should print a result like this:
--- FAIL: TestYamlSuite_Message (0.00s)
yaml_suite_test.go:192:
Error Trace: yaml_suite_test.go:192
Error: Not equal:
expected: "This is a very special test"
actual : ""
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-This is a very special test
+
Test: TestYamlSuite_Message
FAIL
4. Parse YAML and convert it to suite.Suite
This is a little bit complicated and error prone because it is splitted into three steps:
- Parse YAML file
- Convert YAML config structs to runtime test structs
- Assign global configuration
For this take a look at the pkg/suite/yaml_suite.go:ParseYAML
function which is responsible for parsing the suite.
-
Parse yaml -
err := yaml.UnmarshalStrict(content, &yamlConfig)
This line parses the yaml file and will return asuite.YAMLSuiteConf
, later it will be converted to our structs from theruntime
package.Suite::ParseYAML
will then convert the Unmarshalledsuite.YAMLSuiteConf
into asuite.Suite
. Navigate toSuite::NewSuite
and follow the code tos.mergeConfigs
.Jump into the
Suite::mergeConfigs
in [pkg/suite/suite.go] (../pkg/suite/suite.go) and add themessage
property like this:if s.Config.Message == "" { s.Config.Message = config.Message }
as well adding similar logic to
Suite::mergeTestConfigs
pkg/suite/suite.goif s.TestCases[i].Command.Message == "" { s.TestCases[i].Command.Message = s.Config.Message }
-
Convert test cases -
tests := convertYAMLSuiteConfToTestCases(yamlConfig)
Jump into the
convertYAMLSuiteConfToTestCases
function and assign the content of our new field to theruntime.CommandUnderTest
conversion.Command: runtime.CommandUnderTest{ Cmd: t.Command, InheritEnv: t.Config.InheritEnv, [...] Message: t.Config.Message, },
5. Add the global config assignment and add the Message
property
Add a new Message
property to the runtime.GlobalTestConfig
.
type GlobalTestConfig struct {
Env map[string]string
[...]
Message string
}
And last but not least implement the assignment of the global config.
This can be done easily inside the return
of the ParseYAML
function statement.
return Suite{
TestCases: tests,
Config: runtime.TestConfig{
InheritEnv: yamlConfig.Config.InheritEnv,
Env: yamlConfig.Config.Env,
Dir: yamlConfig.Config.Dir,
Timeout: yamlConfig.Config.Timeout,
Retries: yamlConfig.Config.Retries,
Interval: yamlConfig.Config.Interval,
Nodes: yamlConfig.Config.Nodes,
},
Nodes: convertNodes(yamlConfig.Nodes),
}
6. Run the tests
$ make test
INFO: Starting build test
go test ./...
ok github.com/commander-cli/commander/cmd/commander 0.011s
ok github.com/commander-cli/commander/v2/pkg/app 0.014s
ok github.com/commander-cli/commander/v2/pkg/matcher (cached)
ok github.com/commander-cli/commander/v2/pkg/output (cached)
ok github.com/commander-cli/commander/v2/pkg/runtime 0.229s
ok github.com/commander-cli/commander/v2/pkg/suite 0.008s
7. Learning by doing
Two tasks are missing which you can complete on your own.
- Add the printing of the message (tip: Take a look into the
runtime.go
file) - Extend the test case that it is tested that local configs are preferred over global configs (tip: Take a look at the other tests).
Commander tests itself. You can find the integration tests in commander_unix.yaml
and commander_windows.yaml
.
More complex scenarios are stored in integration/
.
It is always necessary to execute the test suite with a stable version of commander and not the current build.
Tipps:
- The working directory is by default the project root, even for tests located inside
integration/*
- Execute
commander
inside thecommander_*.yaml
files with a given suite and assert the result which is returned