2018-09-18-gotest-tools-icmd.org (8161B)
1 #+TITLE: Golang testing — gotest.tools icmd 2 #+date: <2018-09-18 Tue> 3 #+filetags: go testing cmd 4 #+setupfile: ../templates/post.org 5 6 #+TOC: headlines 2 7 8 * Introduction 9 Let's continue the [[https://gotest.tools][=gotest.tools=]] serie, this time with the =icmd= package. 10 11 #+BEGIN_QUOTE 12 Package icmd executes binaries and provides convenient assertions for testing the results. 13 #+END_QUOTE 14 15 After file-system operations (seen in [[/posts/2018-09-14-gotest-tools-fs/][=fs=]]), another common use-case in tests is to 16 *execute a command*. The reasons can be you're testing the =cli= you're currently writing 17 or you need to setup something using a command line. A classic execution in a test might 18 lookup like the following. 19 20 #+BEGIN_SRC go 21 cmd := exec.Command("echo", "foo") 22 cmd.Stout = &stdout 23 cmd.Env = env 24 if err := cmd.Run(); err != nil { 25 t.Fatal(err) 26 } 27 if string(stdout) != "foo" { 28 t.Fatalf("expected: foo, got %s", string(stdout)) 29 } 30 #+END_SRC 31 32 The package =icmd= is there to ease your pain (as usual 😉) — we used /the name =icmd=/ 33 instead of =cmd= because it's a pretty common identifier in Go source code, thus would be 34 really easy to /shadow/ — and have some really weird problems going on. 35 36 The usual =icmd= workflow is the following: 37 38 1. Describe the command you want to execute using : type =Cmd=, function =Command= and 39 =CmdOp= operators. 40 2. Run it using : function =RunCmd= or =RunCommand= (that does 1. for you). You can also 41 use =StartCmd= and =WaitOnCmd= if you want more control on the execution workflow. 42 3. Check the result using the =Assert=, =Equal= or =Compare= methods attached to the 43 =Result= struct that the command execution return. 44 45 * Create and run a command 46 47 Let's first dig how to create commands. In this part, the assumption here is that the 48 command is successful, so we'll have =.Assert(t, icmd.Success)= for now — we'll learn more 49 about =Assert= in the next section 👼. 50 51 The simplest way to create and run a command is using =RunCommand=, it has the same 52 signature as =os/exec.Command=. A simple command execution goes as below. 53 54 #+BEGIN_SRC go 55 icmd.RunCommand("echo", "foo").Assert(t, icmd.Sucess) 56 #+END_SRC 57 58 Sometimes, you need to customize the command a bit more, like adding some environment 59 variable. In those case, you are going to use =RunCmd=, it takes a =Cmd= and operators. 60 Let's look at those functions. 61 62 #+BEGIN_SRC go 63 func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result 64 65 func Command(command string, args ...string) Cmd 66 67 type Cmd struct { 68 Command []string 69 Timeout time.Duration 70 Stdin io.Reader 71 Stdout io.Writer 72 Dir string 73 Env []string 74 } 75 #+END_SRC 76 77 As we've seen [[/posts/2017-01-01-go-testing-functionnal-builders/][multiple]] [[/posts/2018-08-16-gotest-tools-assertions/][times]] [[/posts/2018-09-14-gotest-tools-fs/][before]], it uses the /powerful/ functional arguments. At the 78 time I wrote this post, the =icmd= package doesn't contains too much =CmdOp= [fn:1], so I'll 79 propose two version for each example : one with =CmdOpt= present in [[https://github.com/gotestyourself/gotest.tools/pull/122][this PR]] and one 80 without them. 81 82 #+BEGIN_SRC go 83 // With 84 icmd.RunCmd(icmd.Command("sh", "-c", "echo $FOO"), 85 icmd.WithEnv("FOO=bar", "BAR=baz"), icmd.Dir("/tmp"), 86 icmd.WithTimeout(10*time.Second), 87 ).Assert(t, icmd.Success) 88 89 // Without 90 icmd.RunCmd(icmd.Cmd{ 91 Command: []string{"sh", "-c", "echo $FOO"}, 92 Env: []string{"FOO=bar", "BAR=baz"}, 93 Dir: "/tmp", 94 Timeout: 10*time.Second, 95 }).Assert(t, icmd.Success) 96 #+END_SRC 97 98 As usual, the intent is clear, it's simple to read and composable (with =CmdOp='s). 99 100 [fn:1] The =icmd= package is one of the oldest =gotest.tools= package, that comes from the 101 [[https://github.com/docker/docker][=docker/docker=]] initially. We introduced these =CmdOp= but implementations were in 102 =docker/docker= at first and we never really updated them. 103 104 * Assertions 105 106 Let's dig into the assertion part of =icmd=. Running a command returns a struct 107 =Result=. It has the following methods : 108 109 - =Assert= compares the Result against the Expected struct, and fails the test if any of 110 the expectations are not met. 111 - =Compare= compares the result to Expected and return an error if they do not match. 112 - =Equal= compares the result to Expected. If the result doesn't match expected 113 returns a formatted failure message with the command, stdout, stderr, exit code, and any 114 failed expectations. It returns an =assert.Comparison= struct, that can be used by other 115 =gotest.tools=. 116 - =Combined= returns the stdout and stderr combined into a single string. 117 - =Stderr= returns the stderr of the process as a string. 118 - =Stdout= returns the stdout of the process as a string. 119 120 When you have a result, you, most likely want to do two things : 121 122 - /assert/ that the command succeed or failed with some specific values (exit code, 123 stderr, stdout) 124 - use the output — most likely =stdout= but maybe =stderr= — in the rest of the test. 125 126 As seen above, /asserting/ the command result is using the =Expected= struct. 127 128 #+BEGIN_SRC go 129 type Expected struct { 130 ExitCode int // the exit code the command returned 131 Timeout bool // did it timeout ? 132 Error string // error returned by the execution (os/exe) 133 Out string // content of stdout 134 Err string // content of stderr 135 } 136 #+END_SRC 137 138 =Success= is a constant that defines a success — it's an exit code of =0=, didn't timeout, 139 no error. There is also the =None= constant, that should be used for =Out= or =Err=, to 140 specify that we don't want any content for those standard outputs. 141 142 #+BEGIN_SRC go 143 icmd.RunCmd(icmd.Command("cat", "/does/not/exist")).Assert(t, icmd.Expected{ 144 ExitCode: 1, 145 Err: "cat: /does/not/exist: No such file or directory", 146 }) 147 148 // In case of success, we may want to do something with the result 149 result := icmd.RunCommand("cat", "/does/exist") 150 result.Assert(t, icmd.Success) 151 // Read the output line by line 152 scanner := bufio.NewScanner(strings.NewReader(result.Stdout())) 153 for scanner.Scan() { 154 // Do something with it 155 } 156 #+END_SRC 157 158 If the =Result= doesn't map the =Expected=, a test failure will happen with a useful 159 message that will contains the executed command and what differs between the result and 160 the expectation. 161 162 #+BEGIN_SRC go 163 result := icmd.RunCommand(…) 164 result.Assert(t, icmd.Expected{ 165 ExitCode: 101, 166 Out: "Something else", 167 Err: None, 168 }) 169 // Command: binary arg1 170 // ExitCode: 99 (timeout) 171 // Error: exit code 99 172 // Stdout: the output 173 // Stderr: the stderr 174 // 175 // Failures: 176 // ExitCode was 99 expected 101 177 // Expected command to finish, but it hit the timeout 178 // Expected stdout to contain "Something else" 179 // Expected stderr to contain "[NOTHING]" 180 … 181 #+END_SRC 182 183 Finally, we listed =Equal= above, that returns a =Comparison= struct. This means we can 184 use it easily with the =assert= package. As written in a [[/posts/2018-08-16-gotest-tools-assertions/][previous post (about the =assert= 185 package)]], I tend prefer to use =cmp.Comparison=. Let's convert the above examples using 186 =assert=. 187 188 #+BEGIN_SRC go 189 result := icmd.RunCmd(icmd.Command("cat", "/does/not/exist")) 190 assert.Check(t, result.Equal(icmd.Expected{ 191 ExitCode: 1, 192 Err: "cat: /does/not/exist: No such file or directory", 193 })) 194 195 // In case of success, we may want to do something with the result 196 result := icmd.RunCommand("cat", "/does/exist") 197 assert.Assert(t, result.Equal(icmd.Success)) 198 // Read the output line by line 199 scanner := bufio.NewScanner(strings.NewReader(result.Stdout())) 200 for scanner.Scan() { 201 // Do something with it 202 } 203 #+END_SRC 204 205 * Conclusion… 206 207 … that's a wrap. The =icmd= package allows to easily run command and describe what result 208 are expected of the execution, with the least noise possible. We *use this package heavily* 209 on several =docker/*= projects (the engine, the cli)…