Delving Into Unit Tests

I just got started with Golang a few weeks back and one thing I immediately missed from Python was pdb and pdbpp (a drop in replacement for pdb). Without a debugger I’m floating in print statements for minutes and sometimes hours. Often a debugger can help track down the issue in seconds. After joining CockroachDB (a Go shop through and through), I had the option of installing Goland. But leave my painstakingly tweaked vimrc, tmux.conf, and CLI life behind? Not at this point. I’m confident I can buy my burial plot at this point in my life and I’m thinking of somewhere nice in the green hills of the Vim docs. So what did I find to replace my pdb flow once I moved to Go? Delve, fo sho.

Let’s take some code like the following…

`package mainimport "fmt"// Return the value of the n-th fibonacii number.// 1, 2, 3, 4, 5, 6, 7// 0, 1, 1, 2, 3, 5, 8func fibonacci(n int) (int, error) {    if n < 1 {       return -1, fmt.Errorf("Number is too low: %d", n)    }    if n == 1 {        return 0, nil    }    if n == 2 {        return 1, nil    }    j, k := 0, 1    for i := 3; i <= n; i++ {        j, k = k, j+k    }    return k, nil}func main() {    rng := []int{1, 2, 3, 4, 5, 6}    for x := range rng {        result, err := fibonacci(x)        if err != nil {            fmt.Println(err)        }        fmt.Println(result)    }}`

And some test code like this…

`package mainimport (    "fmt"    "testing")func TestFibonacci(t *testing.T) {    rng := map[int]int{        1: 1,        2: 1,        3: 2,        4: 3,    }    for n, expected := range rng {        n, expected := n, expected        t.Run(fmt.Sprintf("fibonacci(%d)", n), func(t *testing.T) {            t.Parallel()            res, err := fibonacci(n)            if err != nil {                t.Errorf("%w", err)            }            if res != expected {                t.Errorf("fibonacci(%d) = %d; got %d", n, expected, res)            }        })    }}`

Running go test gives me this…

`\$ go test--- FAIL: TestFibonacci (0.00s)    --- FAIL: TestFibonacci/fibonacci(1) (0.00s)        fib_test.go:25: fibonacci(1) = 1; got 0    --- FAIL: TestFibonacci/fibonacci(3) (0.00s)        fib_test.go:25: fibonacci(3) = 2; got 1    --- FAIL: TestFibonacci/fibonacci(4) (0.00s)        fib_test.go:25: fibonacci(4) = 3; got 2FAILexit status 1`

At this point, I’d love to drop a into a debugger right at the start of my test. I tried “dlv debug”, the same command I used to debug my “main” go program with, and couldn’t seem to set break points on the tests. The docs say that “dlv test [package]” is the command to try for that so let’s try…

`\$ dlv test(dlv) break mainCommand failed: Location "main" ambiguous: runtime.main, main.main…(dlv) exit                                                                                                                                                                                           `

`\$ dlv test maincan't load package: package main: cannot find package "main" in any of:        /usr/local/Cellar/go@1.14/1.14.8/libexec/src/main (from \$GOROOT)        /Users/paul/Code/go/src/main (from \$GOPATH)exit status 1`

Hmm, I must be missing something. In Python, I can drop an “import pdb;pdb.set_trace()” anywhere and I’m set. But in Golang, I’m not sure how to do that. It would seem that all things start at “main.main” in compiled world. But with tests, the program doesn’t start there. Let’s try running “dlv test” again and instead breaking on just the test name, TestFibonacci…

`\$ dlv test     (dlv) break TestFibonacciBreakpoint 1 set at 0x115050b for _/Users/paul/Code/dlv-demo.TestFibonacci() ./fib_test.go:8`

Bam!

Stepping through the test function, I hit another snag. I would like to jump into the inner test that is failing. However, when I try to “next” into the function, I get thrown right over it and the loop repeats.

`> _/Users/paul/Code/dlv-demo.TestFibonacci() ./fib_test.go:17 (PC: 0x115079e)    12:                 3: 2,    13:                 4: 3,    14:         }    15:         for n, expected := range rng {    16:                 n, expected := n, expected=>  17:                 t.Run(fmt.Sprintf("fibonacci(%d)", n), func(t *testing.T) {    18:                         t.Parallel()    19:    20:                         res, err := fibonacci(n)    21:                         if err != nil {    22:                                 t.Errorf("%w", err)`

So what are those inner functions inside of our test functions named? Well, there’s a way to find out right from inside dlv. If we want to see all the functions, that exist in the current package, we can run the following command (very similar to “ls *” at the command line) to list them…

`(dlv) funcs TestFi*_/Users/paul/Code/dlv-demo.TestFibonacci_/Users/paul/Code/dlv-demo.TestFibonacci.func1`

Can we break on that function? Yep.

`(dlv) break TestFibonacci.func1Breakpoint 2 set at 0x115094b for _/Users/paul/Code/dlv-demo.TestFibonacci.func1() ./fib_test.go:17`

And could we have broken on a specific line number? Sure thing…

`(dlv) break ./fib_test.go:24Breakpoint 1 set at 0x1150afb for _/Users/paul/Code/dlv-demo.TestFibonacci.func1() ./fib_test.go:24`

I’ve left it up to the reader to determine what the issues is. If you would like to clone the failing tests and run the commands, are repo with both is here: https://github.com/LogstonEducation/dlv-unittest-demo

--

--

--

More from Paul Logston

Engineer, Professor

Love podcasts or audiobooks? Learn on the go with our new app.

Paul Logston

Engineer, Professor