Warm up

A unit test (module test) is a small piece of code written by a developer to verify that a small, well-defined function of the code under test is correct. Typically, a unit test is used to determine the behavior of a particular function under a particular condition (or scenario). Golang also has its own testing package, which can be used to automate unit tests and output verification results.

If you’ve never used Golang’s unit tests before, enter the go Help test command to see the official introduction. Just some key information is printed here:

E:\mygolandproject\MyTest>go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:

        ok   archive/tar   0.011s
        FAIL archive/zip   0.022s
        ok   compress/gzip 0.033s
        ...

followed by detailed output for each failed package.
/ /...
The go tool will ignore a directory named "testdata", making it available
to hold ancillary data needed by the tests.
/ /...
'Go test' recompiles each package along with any files with names matching
the file pattern "*_test.go".
These additional files can contain test functions, benchmark functions, and
example functions. See 'go help testfunc' for more.
/ /...
Copy the code

Go help testfunc again

E:\mygolandproject\MyTest1>go help testfunc
The 'go test' command expects to find test, benchmark, and example functions
in the "*_test.go" files corresponding to the package under test.

A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,

        func TestXxx(t *testing.T){... }/ /...
See the documentation of the testing package for more information.
Copy the code

It should now be clear that to write a test suite, you first need to create a file with a name ending in _test.go that contains the TestXxx function:

func TestXxx(*testing.T)    // XxxIt can be any alphanumeric string, but the first letter cannot be a lowercase letter.
Copy the code

The basic format of the GO test is:

go test [build/test flags] [packages] [build/test flags & test binary flags]
Copy the code

After the go test command is executed, it looks for the TestXxx function in the *_test.go file in the specified package to execute. In addition to some optional flags, pay attention to filling in packages. The *_test.go test file must be stored in the same package as the file to be tested. Run go test or go test. Or go test. /xxx_test.go can run test suites. The test files do not participate in normal source compilation and are not included in the executable.

The go test command ignores the testData directory, which is used to store auxiliary data needed for testing.

When the execution is complete, the result information is printed:

ok   archive/tar   0.011s
FAIL archive/zip   0.022s
...
Copy the code

Unit testing

Code to test:

func Fib(n int) int {
    if n < 4 {
        return n      
    }
    return Fib(n- 1) + Fib(n2 -)}Copy the code

Test code:

func TestFib(t *testing.T) {
    var (
       in       = 7
       expected = 13
    )
    actual := Fib(in)
    fmt.Println(actual)
    ifactual ! = expected {The Errorf() function is used to print formatted error messages in unit tests.
       t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
    }
}
Copy the code

The result is as follows:

E:\myGolandProject\MyTest>go test
PASS
ok      gin/MyTest     0.670s
Copy the code

Change the expected to 14, and the result is as follows:

E:\myGolandProject\MyTest>go test
--- FAIL: TestFib (0.00s)
    first_test.go:15: Fib(7) = 13; expected 14
FAIL
exit status 1
FAIL    gin/MyTest     0.585s
Copy the code

Testing is all about case coverage, and this way, when we have to cover more cases, it’s obviously clumsy to change the code. At this point we can write tests in a table-driven manner, and many tests in the standard library are written in this manner.

func TestFib(t *testing.T) {
    var fibTests = []struct {
        in       int // input
        expected int // expected result
    }{
        {1.1},
        {2.1},
        {3.2},
        {4.3},
        {5.5},
        {6.8},
        {7.13}},for _, tt := range fibTests {
        actual := Fib(tt.in)
        ifactual ! = tt.expected { t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
        }
    }
}
Copy the code

In the example above, if one of the cases fails, test execution will not be terminated.

However, some people may think it is too much trouble to write such a long section of code to test a simple function.

Don’t worry, Goland already has the ability to generate unit test code with one click.

As shown, by placing the cursor over the function name and right-clicking Generate, we can choose to Generate the entire package, the current file, or the test code for the currently selected function. Take the currently selected function as an example, Goland will automatically generate a test file in the current directory, with the following contents:

func TestFib(t *testing.T) {
   type args struct {
      n int
   }
   tests := []struct {
      name string
      args args
      want int} {// TODO: Add test cases.
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         ifgot := Fib(tt.args.n); got ! = tt.want { t.Errorf("Fib() = %v, want %v", got, tt.want)
         }
      })
   }
}
Copy the code

We just need to add the test case to TODO.

Suppose the source file is fib.go and the generated test file is FIB_test.go. If we construct test cases directly and run go test. / FIB_test. go, the following error occurs:

# command-line-arguments [command-line-arguments.test]
.\Fib_test.go:26:14: undefined: Fib
FAIL    command-line-arguments [build failed]
FAIL
Copy the code

Solution: To test a single file, you need to bring the original file to be tested. If the original file has other references, you need to bring the file with you. Run the go test. / fib_test. go command to change it to go test. / fib.go

Continue to explore

So far, the basic flow of Golang unit testing has been introduced. But there is still a question, is * testing.t

TestFib(t * testing.t) is an entry parameter to TestFib(t * testing.t). Let’s go into the source code

// T is a type passed to Test functions to manage test state and support formatted test logs.
//
// A test ends when its Test function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as
// the Parallel method, must be called only from the goroutine running the
// Test function.
//
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
type T struct {
   common
   isParallel bool
   context    *testContext // For running tests and subtests.
}
Copy the code

As you can see, T is the type passed to the Test function to manage the Test state and support formatted Test logs. Test logs are accumulated during test execution and dumped to standard output when the test is complete.

When the test function returns, or when the test function calls FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf, the test function is declared finished. Like the Parallel method, the above methods can only be called from the Goroutine where the test function is running.

Other reporting methods, such as Log and Error variants, can be called simultaneously in multiple Goroutines.

The T type is embedded with the common type, which provides a set of methods that we often use (note that when we say test break, we refer to the current test function, not the entire test file) :

  1. When we encounter an assertion error to mark the test as a failure, we use:
Fail: the test fails and the test continues, i.e., subsequent code still executes FailNow: the test fails and the test function breaksCopy the code

Inside the FailNow method implementation, the test is broken by calling Runtime.goexit ().

  1. When we encounter an assertion error and just want to skip the error and interrupt, but do not want to identify a test failure, we use:
SkipNow: Skip test, test interruptCopy the code

Inside the Implementation of the SkipNow method, the test function is broken by calling Runtime.goexit ().

  1. When we only want to print information, we use:
Log: outputs information Logf: outputs formatting informationCopy the code

Note: By default, the messages printed by unit tests are not printed when they succeed. You can print them by adding the -v option. But for benchmarks, they are always printed out.

  1. When we want to skip the test function and print out the information, we will use:
Skip: Log + SkipNow Skipf: Logf + SkipNowCopy the code
  1. When we want the assertion to fail, mark the test as failing and print the necessary information, but the test function continues to execute, using:
Error: equals Log + Fail Errorf: equals Logf + FailCopy the code
  1. When we want an assertion to fail, mark the test as failed, print the necessary information, but interrupt the test function, using:
Fatal: equal to Log + FailNow Fatalf: equal to Log + FailNowCopy the code

Now let’s look at the definition of runtime.goexit () :

// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.
func Goexit(a){... }Copy the code

The first comment in the function header states that Goexit terminates the goroutine that called it. The question is, why does the test code execute when a test function calls FailNow with an assertion of failure? Doesn’t a Goroutine execute the entire test file? (I did think so at first.) . Actually the answer is in the testing package!

The testing package contains a Runtest function:

// RunTests is an internal function but exported because it is cross-package;
// it is part of the implementation of the "go test" command.
func RunTests(matchString func(pat, str string) (bool, error).tests []InternalTest) (ok bool) {
   var deadline time.Time
   if *timeout > 0 {
      deadline = time.Now().Add(*timeout)
   }
   ran, ok := runTests(matchString, tests, deadline)
   if! ran && ! haveExamples { fmt.Fprintln(os.Stderr,"testing: warning: no tests to run")}return ok
}
Copy the code
  • Runtest is an implementation of the go test command.
  • tests []InternalTestThis slice entry parameter holds all the test functions in the test file
  • RunTests is called and the tests slice entry parameter is passed in

Looking at the internal implementation of the runTests function, I’ve left out other implementation details:

func runTests(matchString func(pat, str string) (bool, error).tests []InternalTest.deadline time.Time) (ran, ok bool) {
  / /...
         tRunner(t, func(t *T) {
            for _, test := range tests {
               t.Run(test.Name, test.F)
            }
         })
  / /...
}
Copy the code

Sure enough, I walked through the Tests slice, calling the Run method for each test function

// Run runs f as a subtest of t called name. It runs f in a separate goroutine
// and blocks until f returns or calls t.Parallel to become a parallel test.
// Run reports whether f succeeded (or at least did not fail before calling t.Parallel).
//
// Run may be called simultaneously from multiple goroutines, but all such calls
// must return before the outer test function for t returns.
func (t *T) Run(name string, f func(t *T)) bool {
   atomic.StoreInt32(&t.hasSub, 1)
   testName, ok, _ := t.context.match.fullName(&t.common, name)
   if! ok || shouldFailFast() {return true
   }
   // Record the stack trace at the point of this call so that if the subtest
   // function - which runs in a separate stack - is marked as a helper, we can
   // continue walking the stack into the parent test.
   var pc [maxStackLen]uintptr
   n := runtime.Callers(2, pc[:])
   t = &T{
      common: common{
         barrier: make(chan bool),
         signal:  make(chan bool.1),
         name:    testName,
         parent:  &t.common,
         level:   t.level + 1,
         creator: pc[:n],
         chatty:  t.chatty,
      },
      context: t.context,
   }
   t.w = indenter{&t.common}

   ift.chatty ! =nil {
      t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
   }
   // Instead of reducing the running count of this test before calling the
   // tRunner and increasing it afterwards, we rely on tRunner keeping the
   // count correct. This ensures that a sequence of sequential tests runs
   // without being preempted, even when their parent is a parallel test. This
   // may especially reduce surprises if *parallel == 1.
   go tRunner(t, f)
   if! <-t.signal {// At this point, it is likely that FailNow was called on one of the
      // parent tests by one of the subtests. Continue aborting up the chain.
      runtime.Goexit()
   }
   return! t.failed }Copy the code

The answer is here. For each f, the test function, a new Goroutine is created to execute! So when a test function fails to assert FailNow, the following test code can be executed because each TestXxx function runs on a different Goroutine.

extension

In Go1.17, a shuffle option was added to Go Test. Shuffle is a shuffle option.

Switch to GO 1.17 and run the go help testFlag command to find the description of -shuffle

/ /...
 -shuffle off,on,N
                Randomize the execution order of tests and benchmarks.
                It is off by default. If -shuffle is set to on, then it will seed
                the randomizer using the system clock. If -shuffle is set to an
                integer N, then N will be used as the seed value. In both cases,
                the seed will be reported for reproducibility.
Copy the code

– Shuffle The default value is off. If it is set to on, shuffle is enabled.

Write a simple Demo to verify this:

import (
   "testing"
)

func TestFunc1(t *testing.T) {
   t.Logf("1")}func TestFunc2(t *testing.T) {
   t.Logf("2")}func TestFunc3(t *testing.T) {
   t.Logf("3")}func TestFunc4(t *testing.T) {
   t.Logf("4")}Copy the code

The result is as follows:

E:\myGolandProject\MyTest>go test -v -shuffle=on .
-test.shuffle 1637545619604654100
=== RUN   TestFunc4
    fib2_test.go:20: 4
--- PASS: TestFunc4 (0.00s)
=== RUN   TestFunc3
    fib2_test.go:16: 3
--- PASS: TestFunc3 (0.00s)
=== RUN   TestFunc1
    fib2_test.go:8: 1
--- PASS: TestFunc1 (0.00s)
=== RUN   TestFunc2
    fib2_test.go:12: 2
--- PASS: TestFunc2 (0.00s)
PASS
ok      command-line-arguments  0.025s
Copy the code

If an error is caused by following a certain test order, it is difficult to locate the error. In this case, the -shuffle option can be used to solve this problem

Reference: www.cnblogs.com/Detector/p/…