Go has everything packaged from the beginning to getting started with testing your code. The language prefers conventions over configuration. By adhering to common naming conventions, Go projects minimize the need for configuring project-specific styles or rules. This is something that the tools for testing is using too of course.
To begin the journey with white box and black box testing we will use a simple package, a calculator, defined in the file calculator.go
.
package calculator
// Calculator defines a simple calculator with basic operations.
type Calculator struct{}
func (c *Calculator) Add(a, b int) int {
return a + b
}
func (c *Calculator) Subtract(a, b int) int {
return a - b
}
func (c *Calculator) Multiply(a, b int) int {
return a * b
}
func isZero(n int) bool {
return n == 0
}
func (c *Calculator) Divide(a, b int) int {
if isZero(b) {
panic("division by zero")
}
return a / b
}
Our tests will live in other files called, calculator_white_box_test.go
and calculator_black_box_test.go
, here is a convention that is important. For the testing tool to find our test-files we will need to use the suffix, _test
, in the filename. The build tool will also exclude this files in the produced binary.
Before we go any further, lets define in simple terms what we mean with white box and black box testing.
White box testing - This refers to the possibility to test all code in your package, both private and public. Therefore all the internal structures and functions (all source code) are available for the tests.
Black box testing - This approach focus more on the functionality since only the public API of the package you are testing is available. The internal implementation of the package is not considered (or available) during these kind of tests.
Ok, so the calculator_white_box_test.go
file could look like this.
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
c := Calculator{}
result := c.Add(1, 2)
if result != 3 {
t.Errorf("Expected 3, but got %d", result)
}
}
func TestIsZero(t *testing.T) {
if !isZero(0) {
t.Errorf("Expected true, but got false")
}
if isZero(1) {
t.Errorf("Expected false, but got true")
}
}
The convention here is if the package name in the test file has the same name as the package we are testing, then this can be considered white box testing. This will enable the possibility to use private functions as isZero
in the tests. Which can be convenient sometimes.
Let us make a file for the black box testing also, calculator_black_box_test.go
.
package calculator_test
import (
"testing"
calculator "white_black_box_testing"
)
func TestDivide(t *testing.T) {
c := calculator.Calculator{}
result := c.Divide(8, 2)
if result != 4 {
t.Errorf("Expected 4, but got %d", result)
}
}
func TestDivideByZero(t *testing.T) {
c := calculator.Calculator{}
defer func() {
if err := recover(); err == nil {
t.Errorf("Expected panic for division by zero, but got none")
}
}()
c.Divide(8, 0)
}
Notice that the package name is, calculator_test
, in this case. That's why we need to import the calculator package. Because of this we only have access to the public API in that package, and therefore we can only do black box testing. So in this file we would not be able to test the isZero
function as we did in the previous test file.
One more convention to notice if you haven't written tests before in Go, is that each test function must start with the prefix, Test
, to enable the test tool to see that test. Without it, the test will never run.
This is how my go.mod
file looks like for completeness of this example.
module white_black_box_testing
go 1.22.0
Which approach should you use? As always, it depends, however I prefer black box testing in most cases.
If you want to learn more about testing your Go code then I can highly recommend this tutorial, Learn Go with Tests.
Happy testing!