Practical Go. Amit Saha
Читать онлайн книгу.with a slice of strings containing the arguments that the program may call and verify the expected behavior. Let's look at the test configurations:
testConfigs := []struct { args []string output string err error }{ // Tests the behavior when no arguments are specified to // the application { args: []string{}, err: errInvalidSubCommand, output: "Invalid sub-command specified\n" + usageMessage, }, // Tests the behavior when "-h" is specified as an argument // to the application { args: []string{"-h"}, err: nil, output: usageMessage, }, // Tests the behavior when an unrecognized sub-command is // to the application { args: []string{"foo"}, err: errInvalidSubCommand, output: "Invalid sub-command specified\n" + usageMessage, }, }
The complete test is shown in Listing 2.6.
Listing 2.6: Unit test for the main
package
// chap2/sub-cmd-arch/handle_command_test.go package main import ( "bytes" "testing" ) func TestHandleCommand(t *testing.T) { usageMessage := `Usage: mync [http|grpc] -h http: A HTTP client. http: <options> server Options: -verb string HTTP method (default "GET") grpc: A gRPC client. grpc: <options> server Options: -body string Body of request -method string Method to call ` // TODO Insert testConfigs from above byteBuf := new(bytes.Buffer) for _, tc := range testConfigs { err := handleCommand(byteBuf, tc.args) if tc.err == nil && err != nil { t.Fatalf("Expected nil error, got %v", err) } if tc.err != nil && err.Error() != tc.err.Error() { t.Fatalf("Expected error %v, got %v", tc.err, err) } if len(tc.output) != 0 { gotOutput := byteBuf.String() if tc.output != gotOutput { t.Errorf("Expected output to be: %#v, Got: %#v", tc.output, gotOutput) } } byteBuf.Reset() } }
Save Listing 2.6 as handle_command_test.go
in the same directory as the main
package (see Listing 2.2).
One behavior for which we haven't written a test is the main
package calling the correct function from the cmd
package when a valid sub-command is specified. Exercise 2.1 gives you an opportunity to do so.
EXERCISE 2.1: TESTING SUB-COMMAND INVOCATION Update the test for the
handleCommand()
function to verify that the correct sub-command implementation is invoked when a valid sub-command is specified. You will find the approach suggested for the solution to Exercise 1.1 useful here as well.
Testing the Cmd Package
To test the cmd
package, you will define similar test cases. Here are the test cases for the TestHandleHttp()
function:
testConfigs := []struct { args []string output string err error }{ // Test behavior when the http sub-command is called with no // positional argument specified { args: []string{}, err: ErrNoServerSpecified, }, // Test behavior when the http sub-command is called with "-h" { args: []string{"-h"}, err: errors.New("flag: help requested"), output: usageMessage, }, // Test behavior when the http sub-command is called // with a positional argument specifying the server URL { args: []string{"http://localhost"}, err: nil, output: "Executing http command\n", }, }
You can find the complete test in chap2/sub-cmd-arch/cmd/handle_http_test.go
.
The test configurations for the TestHandleGrpc()
function are as follows:
testConfigs := []struct { args []string err error output string }{ // Test behavior when the grpc sub-command is called with no // positional argument specified { args: []string{}, err: ErrNoServerSpecified, }, // Test behavior when the grpc sub-command is called with "-h" { args: []string{"-h"}, err: errors.New("flag: help requested"), output: usageMessage, }, // Test behavior when the http sub-command is called // with a positional argument specifying the server URL { args: []string{"-method", "service.host.local/method", "-body", "{}", "http://localhost"}, err: nil, output: "Executing grpc command\n", }, }
You can find the complete test in chap2/sub-cmd-arch/cmd/handle_grpc_test.go
.
The source tree for the application should now look like the following:
. |____cmd | |____grpcCmd.go | |____handle_grpc_test.go | |____handle_http_test.go | |____httpCmd.go | |____errors.go |____handle_command_test.go |____go.mod |____main.go
From the root of the module, run all of the tests:
$ go test -v ./… === RUN TestHandleCommand --- PASS: TestHandleCommand (0.00s) PASS ok github.com/practicalgo/code/chap2/sub-cmd-arch 0.456s === RUN TestHandleGrpc --- PASS: TestHandleGrpc (0.00s) === RUN TestHandleHttp --- PASS: TestHandleHttp (0.00s) PASS ok github.com/practicalgo/code/chap2/sub-cmd-arch/cmd 0.720s
Great. You now have unit tests for both the packages. You have written a test to verify that the main
package displays an error when an empty or invalid sub-command is specified and calls the right sub-command when a valid sub-command is specified. You have also written a test for the cmd
package to verify that the sub-command implementations behave as expected.
In Exercise 2.2, you will add a validation to the http
sub-command to allow only three HTTP methods: GET
, POST
, and HEAD
.
EXERCISE 2.2: HTTP METHOD VALIDATOR You will add validation to the
http
sub-command in this exercise. You will ensure that the method option only allows three values:
GET
(the default),
POST
, and
HEAD
.
If the method yields anything other than these values, the program should exit with a non-zero exit code and print the error “Invalid HTTP method”. Write tests to verify the validation.
In this section, you learned how to write a command-line application with sub-commands. When you are writing a large command-line application, organizing the functionality into separate sub-commands improves the user experience. Next, you will learn how to implement a degree of predictability and robustness in command-line applications.
Making Your Applications Robust
A hallmark of robust applications is that a certain level of control is enforced on their runtime behavior. For example, when your program makes an HTTP request, you may want it to complete within a user-specified number of seconds, and if not, exit with an error message. When such measures are enforced, the program's behavior is more predictable to the user. The context
package in the standard library allows applications to enforce such control. It defines a Context
struct type and three functions— withDeadline()
, withCancel()
, and withTimeout()
—to enforce certain runtime guarantees around your code execution. You will find various standard library packages that require a context object to be passed as the first parameter. Some examples are functions in the net
, net/http
, and os/exec
packages. Although use of contexts is most common when you are communicating with external resources, they are certainly equally applicable to any other functionality where there may be a chance of unpredictable behavior.
User Input with Deadlines
Let's consider an example where your program asks for user input and the user has to type in the input and press the Enter key within 5 seconds else it will move on with a default name. Though a contrived example, it illustrates how you can enforce a time-out on any custom code in your application.
Let's look at the main()
function first: