11.3 Writing test cases

In the course of development, a very important step is to test our code to ensure its quality and integrity. We need to make sure that every function returns the expected result, and that our code performs optimally. We already know that the focus of unit tests is to find logical errors in the design or implementation of programs. They are used to detect and expose problems in code early on so that we can more easily fix them, before they get out of hand. We also know that performance tests are conducted for the purpose of optimizing our code so that it is stable under load, and can maintain a high level of concurrency. In this section, we'll take a look at some commonly asked questions about how unit and performance tests are implemented in Go.

The Go language comes with a lightweight testing framework called testing, and we can use the go test command to execute unit and performance tests. Go's testing framework works similarly to testing frameworks in other languages. You can develop all sorts of test suites with them, which may include tests for unit testes, benchmarking, stress tests, etc. Let's learn about testing in Go, step by step.

How to write test cases

Since the go test command can only be executed in a directory containing all corresponding files, we are going to create a new project directory gotest so that all of our code and test code are in the same directory.

Let's go ahead and create two files in the directory called gotest.go and gotest_test.go

  1. Gotest.go: This file declares our package name and has a function that performs a division operation:

    package gotest
    
     import (
         "errors"
     )
    
     func Division(a, b float64) (float64, error) {
         if b == 0 {
             return 0, errors.New("Divisor can not be 0")
         }
         return a / b, nil
     }
  2. Gotest_test.go: This is our unit test file. Keep in mind the following principles for test files:

  3. File names must end in _test.go so that go test can find and execute the appropriate code

  4. You have to import the testing package
  5. All test case functions begin with Test
  6. Test cases follow the source code order
  7. Test functions of the form TestXxx() take a testing.T argument; we can use this type to record errors or to get the testing status
  8. In functions of the form func TestXxx(t * testing.T), the Xxx section can be any alphanumeric combination, but the first letter cannot be a lowercase letter [az]. For example, Testintdiv would be an invalid function name.
  9. By calling one of the Error, Errorf, FailNow, Fatal or FatalIf methods of testing.T on our testing functions, we can fail the test. In addition, we can call the Log method of testing.T to record the information in the error log.

Here is our test code:

package gotest

import (
    "testing"
)

func Test_Division_1(t *testing.T) {
    // try a unit test on function
    if i, e := Division(6, 2); i != 3 || e != nil { 
        // If it is not as expected, then the test has failed 
        t.Error("division function tests do not pass ") 
    } else {
        // record the expected information
        t.Log("first test passed ") 
    }
}

func Test_Division_2(t *testing.T) {
    t.Error("just does not pass")
}

When executing go test in the project directory, it will display the following information:

--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go: 16: is not passed
FAIL
exit status 1
FAIL gotest 0.013s

We can see from this result that the second test function does not pass since we wrote in a dead-end using t.Error. But what about the performance of our first test function? By default, executing go test does not display test results. We need to supply the verbose argument -v like go test -v to display the following output:

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go: 11: first test passed
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go: 16: is not passed
FAIL
exit status 1
FAIL gotest 0.012s

The above output shows in detail the results of our test. We see that the test function 1 Test_Division_1 passes, and the test function 2 Test_Division_2 fails, finally concluding that our test suite does not pass. Next, we modify the test function 2 with the following code:

func Test_Division_2(t *testing.T) {
    // try a unit test on function
    if _, e := Division(6, 0); e == nil { 
        // If it is not as expected, then the error
        t.Error("Division did not work as expected.") 
    } else {
        // record some of the information you expect to record
        t.Log("one test passed.", e) 
    }
}

We execute go test -v once again. The following information should now be displayed -the test suite has passed~:

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go: 11: first test passed
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
gotest_test.go: 20: one test passed. divisor can not be 0
PASS
ok gotest 0.013s

How to write stress tests

Stress testing is used to detect function performance, and bears some resemblance to unit testing (which we will not get into here), however we need to pay attention to the following points:

  • Stress tests must follow the following format, where XXX can be any alphanumeric combination and its first letter cannot be a lowercase letter.

    func BenchmarkXXX (b *testing.B){...}

  • By default, Go test does not perform function stress tests. If you want to perform stress tests, you need to set the flag -test.bench with the format: -test.bench="test_name_regex". For instance, to run all stress tests in your suite, you would run go test -test.bench=".*".

  • In your stress tests, please remember to use testing.B.N any loop bodies, so that the tests can be run properly.
  • As before, test file names must end in _test.go

Here we create a stress test file called webbench_test.go:

package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { // use b.N for looping
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() // call the function to stop the stress test time count

    // Do some initialization work, such as reading file data, database connections and the like,
    // So that our benchmarks reflect the performance of the function itself

    b.StartTimer() // re-start time
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

We then execute the go test -file webbench_test.go -test.bench =".*" command, which outputs the following results:

PASS
Benchmark_Division 500000000 7.76 ns/ op
Benchmark_TimeConsumingFunction 500000000 7.80 ns/ op
ok gotest 9.364s

The above results show that we did not perform any of our TestXXX unit test functions, and instead only performed our BenchmarkXXX tests (which is exactly as expected). The first Benchmark_Division test shows that our Division() function executed 500 million times, with an average execution time of 7.76ns. The second Benchmark_TimeConsumingFunction shows that our TmeConsumingFunction executed 500 million times, with an average execution time of 7.80ns. Finally, it outputs the total execution time of our test suite.

Summary

From our brief encounter with unit and stress testing in Go, we can see that the testing package is very lightweight, yet packed with useful utilities. We saw that writing unit and stress tests can be very simple, and running them can be even easier with Go's built-in go test command. Every time we modify our code, we can simply run go test to begin regression testing.

results matching ""

    No results matching ""