Warning !!!

All the knowledge presented below is my notes taken while reading this book, intended for research and study purposes. All copyrights belong to the author Adelina Simion of the book Test-Driven Development in Go

Additional test packages

Test files can declare an additional test package, matching the source files package with _test appended.

Tips!

The usage of the dedicated _test package is not enforced in Go, but it is recommended. Whenever possible, you should declare your tests in a dedicated test package.

Using the dedicated test package brings the following advantages:
1. Prevents brittle tests: Restricting access to only exported functionality does not give test code visibility into package internals, such as state variables, which would otherwise cause inconsistent results.
2. Separates test and core package dependencies: In practice, test code will often have its own dedicated verifiers and functionality, which we would not want to be visible to production code.
3. Allows developers to integrate with their own packages

Working with Golang testing package

testing.T: All tests must use this type to interact with the test runner. It contains a method for declaring failing tests, skipping tests, and running tests in parallel.
testing.B: Analogous to the test runner, this type is Go’s benchmark runner. It has the same methods for failing tests, skipping tests, and running benchmarks in parallel. Benchmarks are special kinds of tests that are used for verifying the performance of your code, as opposed to its functionality.
testing.F: This type is used to set up and run fuzz tests and was added to the Go standard toolchain in Go 1.18. It creates a randomized seed for the testing target and works together with the testing.T type to provide test-running functionality. Fuzz tests are special kinds of tests that use random inputs to find edge cases and bugs in our code.

t.Log(args): This prints the given arguments to the error log after the test has finished executing.
t.Fail(): This marks the current test as failed but continues execution until the end.
t.FailNow(): This marks the current test as failed and immediately stops the execution of the current test. The next test will be run while continuing the suite.
t.Error(args): This is equivalent to calling t.Log(args) and t.Fail(). This method makes it convenient to log an error to the error log and mark the current test as failed.
t.Fatal(args): This is equivalent to calling t.Log(args) and t.FailNow(). This method makes it convenient to fail a test and print an error line in one call.
t.SkipNow(): This marks the current test as skipped and immediately stops its execution. Note that if the test has already been marked as failed, then it remains failed, not skipped. t.Skip(args): This is equivalent to calling t.Log(args), followed by t.SkipNow(). This method makes it convenient to skip a test and print an error line in one call.

Test signatures

func TestName(t *testing.T) {
  // implementation
}

– Tests are exported functions whose name begins with Test
– Test names can have an additional suffix that specifies what the test is covering
– Tests must take in a single parameter of the *testing.T type. for the test interact with the test runner
– Tests must not have a return type.

How to name a Test ?

TestUnitUnderTest structure: For example, a test for the Add function will be named TestAdd.
Behavior-Driven Development (BDD) style approach: name of the test follows the structure of TestUnitUnderTest_PreconditionsOrInputs_ExpectedOutput. For example, a test for the function will be named TestAdd_TwoNegativeNumbers_NegativeResults if it tests adding two negative numbers together.

Running tests

go test Tips!

add –v flag : print the name and execution time of all tests, including passed tests
We can easily specify what tests to run by providing these properties:
* A specific package name: For example, go test engine_test will run the tests from the engine_test package from anywhere in the project directory.
* The expression as the package identifier: For example, go test ./… will run all the tests in the project, regardless of where it’s being run from.
* A subdirectory path: For example, go test ./chapter02 will run all the tests in the chapter02 subdirectory of the current path, but will not traverse to further nested directories.
* A regular expression, together with the –run flag: For example, go test –run “^engine” will run all packages that begin with the word engine. A subdirectory path can also be provided alongside the test name.
* A test name, together with the –run flag: For example, go test –run TestAdd will only the test specified. A subdirectory path can also be provided alongside the test name.

The Go test runner can cache successful test results to avoid wasting resources by rerunning tests on code that has not changed. Being able to cache successful test results is disabled by default when running in local directory mode, but enabled in package list mode. (go test –v ./… )

Write tests

Test setup and teardown

when we write more test, it is difficult to continue repeating the same test setup and cleanup
→ using TestMain(): when it is present within a package, it becomes the main entry point for running the tests within that package.

func TestMain(m *testing.M) {
  // setup statements
  setup()
  // run the tests
  e := m.Run()
  // cleanup statements
  teardown()
  // report the exit code
  os.Exit(e)
}

func setup() {
  log.Println("Setting up.")
}
func teardown() {
  log.Println("Tearing down.")
}

The procedure will be like this:
1. call: go test
2. The init functions execute before the temporary main program of the tests.
3. Once the tests are ready to execute, the TestMain function starts and its setup functions execute.
4. The tests are then run by invoking m.Run() from TestMain.
5. Once all the tests have been run, the deferred functions defined inside the scope of the tests are executed.
6. Once the tests and their functions exit, the TestMain function’s teardown function is executed.
7. Finally, the tests end with the exit value returned from the call to m.Run().

Subtests

What if we want to extend more test cases inside the test ? EX: adding test case for adding 2 negative number inside Add(int, int) test

The testing.T type provides the Run(name string, f func(t *testing.T)) bool method. Once passed to the Run method, the test runner will run the function as a subtest of the current tests, allowing us to create a test hierarchy, each with its own separation. Since the enclosing test and the subtests share the same instance of testing.T, a subtest failure will cause the enclosing test to fail as well. This behavior gives us the ability to create multi-layered test hierarchies according to our needs

func TestAdd(t *testing.T) {
  // Arrange
  e := calculator.Engine{}
  actAssert := func(x, y, want float64) {
  // Act
  got := e.Add(x, y)
  //Assert
  if got != want {
    t.Errorf("Add(%.2f,%.2f) incorrect, got: %.2f, want:
      %.2f", x, y, got, want)
  }
}
  t.Run("positive input", func(t *testing.T) {
    x, y := 2.5, 3.5
    want := 6.0
    actAssert(x, y, want)
  })
  t.Run("negative input", func(t *testing.T) {
    x, y := -2.5, -3.5
    want := -6.0
    actAssert(x, y, want)
  })
}
$ go test -run "^TestAdd" ./chapter02/calculator -v 
=== RUN   TestAdd
=== RUN   TestAdd/positive_input
=== RUN   TestAdd/negative_input
--- PASS: TestAdd (0.00s)
    --- PASS: TestAdd/positive_input (0.00s)
    --- PASS: TestAdd/negative_input (0.00s)
PASS
ok      github.com/PacktPublishing/Test-Driven-Development-in-Go/chapter02/calculator   0.195s

Categorized in:

Tagged in:

, ,