Practical Go. Amit Saha
Читать онлайн книгу.in the
-v
flag when running go test
also displays the test functions that are being run and the result.
Next, consider the validateArgs()
function defined as func validateArgs(c config) error
. Based on the function specification, we will once again define a slice of test cases. However, instead of defining a named struct
type, we will use an anonymous struct
type instead as follows:
tests := []struct { c config err error }{ { c: config{}, err: errors.New("Must specify a number greater than 0"), }, { c: config{numTimes: -1}, err: errors.New("Must specify a number greater than 0"), }, { c: config{numTimes: 10}, err: nil, }, }
Each test case consists of two fields: an input object, c
, of type config
, and the expected error
value, err
. The test function is shown in Listing 1.3.
Listing 1.3: Test for the validateArgs()
function
// chap1/manual-parse/validate_args_test.go package main import ( "errors" "testing" ) func TestValidateArgs(t *testing.T) { // TODO Insert definition tests[] slice as above for _, tc := range tests { err := validateArgs(tc.c) if tc. err != nil && err.Error() != tc.err.Error() { t.Errorf("Expected error to be: %v, got: %v\n", tc.err, err) } if tc.err == nil && err != nil { t.Errorf("Expected nil error, got: %v\n", err) } } }
In the same subdirectory as Listing 1.2, save Listing 1.3 to a file called validate_args_test.go
. Now run the tests using the go test
command. It will now run both the TestParseFlags
and TestValidateArgs
tests.
Finally, you will write a unit test for the runCmd()
function. This function has the signature runCmd(r io.Reader, w io.Writer, c config)
. We will define a set of test cases as follows:
tests := []struct { c config input string output string err error }{ { c: config{printUsage: true}, output: usageString, }, { c: config{numTimes: 5}, input: "", output: strings.Repeat("Your name please? Press the Enter key when done.\n", 1), err: errors.New("You didn't enter your name"), }, { c: config{numTimes: 5}, input: "Bill Bryson", output: "Your name please? Press the Enter key when done.\n" + strings.Repeat("Nice to meet you Bill Bryson\n", 5), }, }
The field c
is a config
object representing the incoming configuration, input
is the test input received by the program from the user interactively, output
is the expected output, and err
represents any error that is expected based on the test input and configuration.
When you write a test for a program where you have to mimic an input from the user, this is how you can create a io.Reader
from a string:
r := strings.NewReader(tc.input)
Thus, when the getName()
function is called with io.Reader r
as created above, calling scanner.Text()
will return the string in tc.input
.
To mimic the standard output, we create an empty Buffer
object that implements the Writer
interface using new(bytes.Buffer)
. We can then obtain the message that was written to this Buffer
using the byteBuf.String()
method. The complete test is shown in Listing 1.4.
Listing 1.4: Test for the runCmd()
function
// chap1/manual-parse/run_cmd_test.go package main import ( "bytes" "errors" "strings" "testing" ) func TestRunCmd(t *testing.T) { // TODO Insert definition tests[] array as earlier byteBuf := new(bytes.Buffer) for _, tc := range tests { rd := strings.NewReader(tc.input) err := runCmd(rd, byteBuf, tc.c) if err != nil && tc.err == nil { t.Fatalf("Expected nil error, got: %v\n", err) } if tc.err != nil && err.Error() != tc.err.Error() { t.Fatalf("Expected error: %v, Got error: %v\n", tc.err.Error(), err.Error()) } gotMsg := byteBuf.String() if gotMsg != tc.output { t.Errorf("Expected stdout message to be: %v, Got: %v\n", tc.output, gotMsg) } byteBuf.Reset() } }
We call the byteBuf.Reset()
method so that the buffer is emptied before executing the next test case. Save Listing 1.4 into the same directory as Listings 1.1, 1.2, and 1.3. Name the file run_cmd_test.go
and run all of the tests:
$ go test -v === RUN TestParseArgs --- PASS: TestParseArgs (0.00s) === RUN TestRunCmd --- PASS: TestRunCmd (0.00s) PASS ok github.com/practicalgo/code/chap1/manual-parse 0.529s
You may be curious to find out what the test coverage looks like and visually see which parts of your code are not tested. To do so, run the following command first to create a coverage profile:
$ go test -coverprofile cover.out PASS coverage: 71.7% of statements ok github.com/practicalgo/code/chap1/manual-parse 0.084s
The above output tells us that our tests cover 71.7 percent of the code in main.go
. To see which parts of the code are covered, run the following:
$ go tool cover -html=cover.out
This will open your default browser application and show the coverage of your code in an HTML file. Notably, you will see that the main()
function is reported as uncovered since we didn't write a test for it. This leads nicely to Exercise 1.1.
EXERCISE 1.1: TESTING THE MAIN() FUNCTION In this exercise, you will write a test for the
main()
function. However, unlike with other functions, you will need to test the exit status for different input arguments. To do so, your test should do the following:
1 Build the application. You will find using the special TestMain() function useful here.
2 Execute the application with different command-line arguments using the os.Exec() function. This will allow you to verify both the standard output and the exit code.
Congratulations! You have written your first command-line application. You parsed the os.Args
slice to allow the user to provide input to the application. You learned how to make use of the io
. Reader
and io
. Writer
interfaces to write code that is unit testable.
Next, we will see how the standard library's flag
package automatically takes care of the command-line argument parsing, validation of the type of data, and more.
Using the Flag Package
Before we dive into the flag
package, let's refresh our memory of what a typical command-line application's user interface looks like. Let's consider a command-line application called application
. Typically, it will have an interface similar to the following:
application [-h] [-n <value>] –silent <arg1> <arg2>
The user interface has the following components:
-h is a Boolean option usually specified to print a help text.
-n <value> expects the user