👋 Welcome Gophers! In this video, we are going to be looking at how you can effectively write sub-tests in Go!
In the previous tutorial, we started our journey writing tests in Go and wrote our first 2 unit tests whilst also trying to understand a little better why we write tests to cover our applications.
In this section of the course, we’re going to take things a little bit further and improve the way we do write our tests by incoporating sub-tests into our Go testing strategy.
The t.Run Method
When we write tests, you may have noticed we pass in this pointer t
which is a type that has been defined in the testing
package in Go’s standard library. This type T
is passed to all testing functions to help manage the state of all of our tests and also helps with some other auxilliary tasks like formatted test logs.
This type has a number of different methods based which we can use within our tests which range from Fail
which we’ve already seen in the last tutorial, through to methods like Skip
or the method we are going to look at now which is t.Run
.
The t.Run
method effectively enables us to define subtests within our Test
functions. It effectively gives us more power when writing tests to incorporate things like parallelism in our tests.
Refactoring our Tests
Now, before we do anything, we should sanity check that adding new functionality to our package has not somehow broken our existing tests:
$ go test ./...
Perfect, we’ve now been able to validate that our code hasn’t broken anything and we can start refactoring our tests to use subtests!
Let’s dive in to our calculator_test.go
file and start modifying the existing tests and add a few more in for good measure:
package calculator_test
import "testing"
type TestCase struct {
value int
expected bool
actual bool
}
func TestCalculateIsArmstrong(t *testing.T) {
t.Run("should return true for 371", func(t *testing.T) {
testCase := TestCase{
value: 371,
expected: true,
}
testCase.Actual = CalculateIsArmstrong(testCase.value)
if testCase.Actual != testCase.expected {
t.Fail("CalculateIsArmstrong returned an unexpected result")
}
})
t.Run("should return true for first armstrong number", func(t *testing.T) {
testCase := TestCase{
value: 1,
expected: true,
}
testCase.Actual = CalculateIsArmstrong(testCase.value)
if testCase.Actual != testCase.expected {
t.Fail("CalculateIsArmstrong returned an unexpected result")
}
})
t.Run("should return false for non-armstrong number", func(t *testing.T){
testCase := TestCase{
value: 15,
expected: false,
}
testCase.Actual = CalculateIsArmstrong(testCase.value)
if testCase.Actual != testCase.expected {
t.Fail("CalculateIsArmstrong returned an unexpected result")
}
})
}
Now, you may be wondering what the point of this is? When you start building up more complex Go applications this quickly becomes apparent.
When you develop more complex apps, the number of tests that app has should start to grow quickly. Therefore the time taken to run through all of the tests becomes increasingly longer.
By grouping all of our tests as subtests under a standard test, we can quickly and easily run the full suite of tests for this particular function using the -run flag:
$ go test -run TestCalculateIsArmstrong
This will go away and run only the subtests pertinent to the function we are working in and allows reduces the amount of time between us changing a line of code, and validating that our tests still pass with these new changes!
Conclusion
Awesome, in this tutorial, we’ve covered the wonders of subtests and how they can be beneficial to you in your pursuits as a Go developer.
In the next tutorial in this course, we’ll be taking it another step further and looking at how we can use table-driven tests to further improve how we test our applications!