Codementor Events

How Table Driven Tests makes writing unit tests exciting and fun in Go

Published Apr 17, 2020
How Table Driven Tests makes writing unit tests exciting and fun in Go

I remember my earlier days as a software developer when I found duplicating codes to be a cool thing. It doesn’t mean that I didn’t know DRY principle back then, but code duplication comes habitually when you try to do something without caring about it. And in most cases, you don’t care about things that is not exciting to you.

Testing is the first thing a beginner (or a cowboy) coder neglects more than the math book on his/her shelf. He/she tries to avoid testing at all, but for some reasons (like pressure from seniors) he/she writes tests. But it contains similar duplicate codes or misses many edge cases.

We developers who cares about what we write always hate code duplication. This is why there’s a sound principle called DRY. Which stands for Don’t Repeat Yourself. Yes it is. Don’t ever ever ever duplicate code here and there from now on. It’s a serious pain for future maintenance.

We all know how important testing is. But writing tests seems tedious. So, we need to use some clever tricks to make writing tests fun and easy. There are many ways to handle this. But I found two ways working great:

  1. Keep the tests as fast as possible no matter how many test cases are
  2. Keep the tests writing part as short and exciting as possible

In order to keep the test runs instantly, we need to diagnose what keeps our tests to run slow as hog. In most cases, this is the I/O part that sits inside of every business functions. By I/O part, I meant things like Database Calls, Network Calls etc.

But we need this things, right? They are equally important. This is where we need to use Abstractions and Dependency Injection. This two things will make sure our tests run fast by supplying a mock implementation of the abstraction at runtime by using Dependency Injection. This is a whole new topic of it’s own. I’ll write an article on it.

Let’s take a look at the second part.

If you’re a Go developer you should find this code familiar:

package reversestring

import (
   "strings"
)

func ReverseString(s string) string {
   if s == "" {
      return ""
   }
   
   var newString []string
   for i := len(s)-1; i >= 0; i-- {
      newString = append(newString, string(s[i]))
   }
   return strings.Join(newString, "")
}

This is a simple function that reverses a string. Now tell me how do you test this?

You start by checking empty string result:

package reversestring

import (
   "testing"
)

func TestReverseString(t *testing.T) {
   if ReverseString("") != "" {
      t.Fail()
   }
}

Then you run:

go test -v

And then you write another test case:

package reversestring

import (
   "testing"
)

func TestReverseString(t *testing.T) {
   if ReverseString("") != "" {
      t.Fail()
   }
   
   if ReverseString("hello") != "olleh" {
      t.Fail()
   }
}

And then running the test again. Then add another one, run test……and the cycle continues.

BAMM!

It feels so boring, right? There’s no excitement in writing test cases like this.
We all know Go is an exciting and fun language. So there should be exciting and fun way to write unit tests in Go. Let’s see what it is.
Also, can you see code duplication here? Only the string part changes here, other remains the same. But we need to write a new If…else condition for every new string test cases.

That’s not an efficient way. Right?

This is where table driven tests comes in handy. Let’s see how it looks like:

package reversestring

import (
   "testing"
)

func TestReverseString1(t *testing.T) {
   type args struct {
      s string
   }
   tests := []struct {
      name string
      args args
      want string
   }{
      // write your test case here....
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := ReverseString(tt.args.s); got != tt.want {
            t.Errorf("ReverseString() = %v, want %v", got, tt.want)
         }
      })
   }
}

This is the boilerplate setup code for a table driven test. It sets up everything for you. You just need to give input to the function and the expected output of it. Rest of the things like asserting, checking errors etc will be handled by our neatly written table driven tests.

This code may seem a little bit different to you if you follow the official go guide on table driven tests (which can be found here).

This is slightly modified version of the official one. I have generated it using the awesome gotests package. It has supports for major text editors and IDE. Please check it out.

Ok, back to the code again. The interesting thing is, we can now write tests in declarative way rather than imperative way. We just tell “I want to supply this and this input to the function and I want to check if the output is this or not” and this will be done for you. Exciting, right?

Let’s see it in action:

package reversestring

import (
   "testing"
)

func TestReverseString1(t *testing.T) {
   type args struct {
      s string
   }
   tests := []struct {
      name string
      args args
      want string
   }{
      {
         name: "testing empty",
         args: args{
            s:"",
         },
         want: "",
      },
      {
         name: "testing hello",
         args: args{
            s:"hello",
         },
         want: "olleh",
      },
      {
         name: "testing question",
         args: args{
            s:"why am I?",
         },
         want: "?I ma yhw",
      },
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := ReverseString(tt.args.s); got != tt.want {
            t.Errorf("ReverseString() = %v, want %v", got, tt.want)
         }
      })
   }
}

Here, I write the test case by giving a name to it, passing input argument and the output I want. That’s all about it. It seems so natural and interesting right now. We don’t need to write a whole new function or if…else case for each of the test case.

We can now run the test:

go test -v

And the output is:

=== RUN TestReverseString1
=== RUN TestReverseString1/testing_empty
=== RUN TestReverseString1/testing_hello
=== RUN TestReverseString1/testing_question
 — — PASS: TestReverseString1 (0.00s)
 — — PASS: TestReverseString1/testing_empty (0.00s)
 — — PASS: TestReverseString1/testing_hello (0.00s)
 — — PASS: TestReverseString1/testing_question (0.00s)
PASS
ok reversestring 0.003s

Awesome. Now you can add a new test case by using a name, input and expected output:

{
   name: "testing fruit name",
   args: args{
      s:"mango",
   },
   want: "ognam",
},

So now, writing unit tests will be less burden to you. You don’t need to write duplicate codes.

I think you should never write test cases without using table driven tests again.
So, that’s all about it. I’ve included some reading list at the end of this article. Check ’em out.

https://github.com/golang/go/wiki/TableDrivenTests
https://dave.cheney.net/2019/05/07/prefer-table-driven-tests
https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go
https://ieftimov.com/post/testing-in-go-table-driven-tests/
https://medium.com/@pliutau/table-driven-tests-in-go-5d7e230681da
https://medium.com/@cep21/closure-driven-tests-an-alternative-style-to-table-driven-tests-in-go-628a41497e5e
https://yourbasic.org/golang/table-driven-unit-test/
https://jayson.dev/blog/2019/06/table-driven-golang-subtests/
https://dzone.com/articles/table-driven-tests-in-go

Discover and read more posts from Cyan Tarek
get started
post commentsBe the first to share your opinion
Show more replies