2023 09 20
We’re still just getting started with building out our testing framework. Last time we broke out our code into multiple files, but we’re not done with the refactoring step of this first requirement yet. Our run
method calls the testMethod
directly, regardless of the name we pass into the Test
factory function. Because we have a working (though still partly manual) unit test, we don’t have to “generalize purely with reasoning”, as Kent Beck states in Test-Driven Development (which is the guide with which we’re following along to build this framework). In contrast, we have a concrete example as the starting point, and we can generalize from there.
Our concrete example, from last week, looked like this:
func (t *Test) Run() {
t.testMethod()
}
In order to generalize this, we need to call the method based on the name given to us by the factory method caller, rather than calling testMethod()
directly. That means the name of the method won’t be known at compile-time, and we need to use reflection.
Reflection in Go (and in most languages) is a deep topic, and I’ll create a post that goes into more detail about it another time. For now, we simply want to call a method based on the method name given to us as a string. We can probably do this safely without an intimate understanding of reflection. The refactored version looks like this (be sure and import the reflect
library from Go’s standard library):
func (t *TestCase) Run() {
method := reflect.ValueOf(t).MethodByName(t.name)
method.Call(nil)
}
I also changed the name of the Test
struct to be TestCase
, because it makes things a little bit more readable overall, and it maintains consistency with Kent Beck’s approach.
Now, let’s finally take some of the manual work out of running our test by writing up a simple assert
method. In a new file assert.go
, I’ve written
func Assert(expression bool) {
if !expression {
fmt.Println("Assertion failed.")
}
}
This function is overly simplistic, but we’ll dress it up a little later. For now, it allows us to merely have to check if any of our assertions fail, meaning that a successful run will have no output. If we want our “stuff under test” to have output of it’s out, this could get a little confusing, but we’re going to continue to worry about one thing at a time.
Now, our code looks like this:
TestCase.go
package main
import (
"reflect"
)
type TestCase struct {
name string
wasRun bool
}
func NewTestCase(name string) *TestCase {
test := TestCase{name: name}
test.wasRun = false
return &test
}
func (t *TestCase) TestMethod() {
t.wasRun = true
}
func (t *TestCase) Run() {
method := reflect.ValueOf(t).MethodByName(t.name)
method.Call(nil)
}
TestTestRunner.go
package main
func main() {
test := NewTestCase("TestMethod")
Assert(!test.wasRun)
test.Run()
Assert(test.wasRun)
}
Our TestCase type is in a decent state and our tests are passing. Next time, we’ll start to add more features to our framework. Looking forward to it!
Thanks for reading,
Alec