Practical Go. Amit Saha
Читать онлайн книгу."errors" "fmt" "io" "os" "strconv" ) type config struct { numTimes int printUsage bool } var usageString = fmt.Sprintf(`Usage: %s <integer> [-h|--help] A greeter application which prints the name you entered <integer> number of times. `, os.Args[0]) func printUsage(w io.Writer) { fmt.Fprintf(w, usageString) } func validateArgs(c config) error { if !(c.numTimes> 0) { return errors.New("Must specify a number greater than 0") } return nil } // TODO – Insert definition of parseArgs() as earlier // TODO – Insert definition of getName() as earlier // TODO – Insert definition of greetUser() as earlier // TODO – Insert definition of runCmd() as earlier func main() { c, err := parseArgs(os.Args[1:]) if err != nil { fmt.Fprintln(os.Stdout, err) printUsage(os.Stdout) os.Exit(1) } err = validateArgs(c) if err != nil { fmt.Fprintln(os.Stdout, err) printUsage(os.Stdout) os.Exit(1) } err = runCmd(os.Stdin, os.Stdout, c) if err != nil { fmt.Fprintln(os.Stdout, err) os.Exit(1) } }
The main()
function first calls the parseArgs()
function with the slice of the command-line arguments, starting from the second argument. We get back two values from the function: c
, a config object, and err
, an error value. If a non- nil
error is returned, the following steps are performed:
1 Print the error.
2 Print a usage message by calling the printUsage() function, passing in os.Stdout as the writer.
3 Terminate the program execution with exit code 1 by calling the Exit() function from the os package.
If the arguments have been parsed correctly, the validateArgs()
function is called with the config object, c
, that is returned by parseArgs()
.
Finally, if the validateArgs()
function returned a nil
error value, the runCmd()
function is called, passing it a reader, os.Stdin
; a writer, os.Stdout
; and the config object, c
.
Create a new directory, chap1/manual-parse/
, and initialize a module inside it:
$ mkdir -p chap1/manual-parse $ cd chap1/manual-parse $ go mod init github.com/username/manual-parse
Next, save Listing 1.1 to a file called main.go
, and build it:
$ go build -o application
Run the command without specifying any arguments. You will see an error and the following usage message:
$ ./application Invalid number of arguments Usage: ./application <integer> [-h|--help] A greeter application which prints the name you entered <integer> number of times.
In addition, you will also see that the exit code of the program is 1
.
$ echo $? 1
If you are using PowerShell on Windows, you can use echo $LastExitCode
to see the exit code.
This is another notable behavior of command-line applications that you should look to preserve. Any non-successful execution should result in a non-zero exit code upon termination using the Exit()
function defined in the os
package.
Specifying -h
or -help
will print a usage message:
$ ./application -help Usage: ./application <integer> [-h|-help] A greeter application which prints the name you entered <integer> number of times.
Finally, let's see what a successful execution of the program looks like:
$ ./application 5 Your name please? Press the Enter key when done. Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool
You have manually tested that your application behaves as expected under three different input scenarios:
1 No command-line argument specified.
2 -h or -help is specified as a command-line argument.
3 A greeting is displayed to the user a specified number of times.
Manual testing is error prone and cumbersome, however. Next, you will learn to write automated tests for your application.
Writing Unit Tests
The standard library's testing
package contains everything you need to write tests to verify the behavior of your application.
Let's consider the parseArgs()
function first. It is defined as follows:
func parseArgs(args []string) (config, error) {}
It has one input: a slice of strings representing the command-line arguments specified to the program during invocation. The return values are a value of type config
and a value of type error
.
The testConfig
structure will be used to encapsulate a specific test case: a slice of strings representing the input command-line arguments in the args
field, expected error value returned in the err
field, and the expected config
value returned in the embedded config
struct field:
type testConfig struct { args []string err error config }
An example test case is
{ args: []string{"-h"}, err: nil, config: config{printUsage: true, numTimes: 0}, },
This test case verifies the behavior when -h
is specified as the command-line argument when executing the application.
We add a few more test cases and initialize a slice of test cases as follows:
tests := []testConfig{ { args: []string{"-h"}, err: nil, config: config{printUsage: true, numTimes: 0}, }, { args: []string{"10"}, err: nil, config: config{printUsage: false, numTimes: 10}, }, { args: []string{"abc"}, err: errors.New("strconv.Atoi: parsing \"abc\": invalid syntax"), config: config{printUsage: false, numTimes: 0}, }, { args: []string{"1", "foo"}, err: errors.New("Invalid number of arguments"), config: config{printUsage: false, numTimes: 0}, }, }
Once we have defined the slice of test configurations above, we will iterate over them, invoke the parseArgs()
function with the value in args
, and check whether the returned values, c
and err
, match the expected values of type config
and error
, respectively. The complete test will appear as shown in Listing 1.2.
Listing 1.2: Test for the parseArgs()
function
// chap1/manual-parse/parse_args_test.go package main import ( "errors" "testing" ) func TestParseArgs(t *testing.T) { // TODO Insert definition tests[] array as earlier for _, tc := range tests { c, err := parseArgs(tc.args) if tc.result.err != nil && err.Error() != tc.result.err.Error() { t.Fatalf("Expected error to be: %v, got: %v\n", tc.result.err, err) } if tc.result.err == nil && err != nil { t.Errorf("Expected nil error, got: %v\n", err) } if c.printUsage != tc.result.printUsage { t.Errorf("Expected printUsage to be: %v, got: %v\n", tc.result.printUsage, c.printUsage) } if c.numTimes != tc.result.numTimes { t.Errorf("Expected numTimes to be: %v, got: %v\n", tc.result.numTimes, c.numTimes) } } }
In the same directory as you saved Listing 1.1, save Listing 1.2 into a file called parse_flags_test.go
. Now run the test using the go test
command:
$ go test -v === RUN TestParseArgs --- PASS: TestParseArgs (0.00s) PASS ok github.com/practicalgo/code/chap1/manual-parse 0.093
Passing