Warning !!!
Additional test packages
Test files can declare an additional test package, matching the source files package with _test appended.
Tips!
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 ?
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!
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