Practical Go. Amit Saha
Чтение книги онлайн.
Читать онлайн книгу Practical Go - Amit Saha страница 13
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 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