sunnuntai 22. toukokuuta 2016

Testing with Golang

Coming from a Java background, testing with Go has been a struggle from the beginning for me. For simple unit testing, there are simple enough guides for doing it, but what about the more complex cases? What if I use a library like Echo, should I test what a middleware function returns? How do I mock data needed by the function?

And there’s of course the simplicity of Go, everything is so small and nice, should I even test the smallest functions? I’ll share some of my experiences and thoughts about testing in Go.




Unit testing

Here's a function which drops everything that is not a letter or a number:
func validate(param string) string {
 r, err := regexp.Compile("[a-zåäöA-ZÅÄÖ0-9]+")
 if err != nil {
  fmt.Fatal(err)
 }
 return string(r.ReplaceAll([]byte(param), []byte(""))[:])
}

Testing this is easy enough:
func TestValidateSuccess(t *testing.T) {
    value := validate("foo bar ! 1 2[]()><")
    if value != "foobar12" {
        t.Errorf("Validate result was not 'foobar'")
    }
}

One thing to notice is that if you make all functions to return values, it's really easy to test. If you modify values without returning them, testing is a bit harder job to do (At least for me).

There are extensions to testing package also. For example testify (https://github.com/stretchr/testify).
With it you can easily do assertions with a nice syntax. It also supports mocking.
func TestValidateSuccess(t *testing.T) {
    assert := assert.New(t)
    assert.Equal(validate("foo bar ! 1 2[]()><"), "foobar12", "should be equal")
}

That was a test for a sunny day, how about testing for panics?
Say I have a book shelf which can hold certain amount of books.
type shelf struct {
 books []book
}

type book struct {
 name string
}

var myShelf = shelf{books: make([]book, 5)}

Ok, I now have a shelf which has room for five books. I also need a function to add books to the shelf:
func addBookToShelf(book book, i int) {
 myShelf.books[i] = book
}

These are the books I want to put on the shelf, you might see the problem already.
var conanDoyle = []string{
 "A Study in Scarlet",
 "The Sign of Four",
 "The Adventures of Sherlock Holmes",
 "The Memoirs of Sherlock Holmes",
 "The Hound of the Baskervilles",
 "The Return of Sherlock Holmes",
 "The Valley of Fear",
}

The loop to add the books with addBookToShelf function:
for i, b := range conanDoyle {
 addBookToShelf(book{name: b}, i)
}

This panics a runtime error for index out of range. How can I test this?

By searching the panic for example
func TestAddBooksToShelf(t *testing.T) {
    defer func() {
        if r := recover(); r != nil {
            t.Errorf("handleBooks() panicked, shouldn't have")
        }
    }()
    handleBooks()
}

This is how you can catch panics in a test and not in production, it is also useful in cases you really want to panic.

Smoke or sanity testing

This is usually done right before going to production. It might be all your other tests which are run when you're making for example your Docker image, but if you have lot's of slow tests, this is then usually not the case.

It might be a shell script with a set of curl requests to check that the most important 
endpoints are answering. Or it might be a test like this, you could easily add more checking:
func TestGetUrls(t *testing.T) {
 for i := 0; i < 5; i++ {
  go makeHttpGet(t, "http://localhost:2000/endpoint1")
  go makeHttpGet(t, "http://localhost:2000/endpoint2")
  go makeHttpGet(t, "http://localhost:2000/endpoint3")
 }
}

func makeHttpGet(t *testing.T, url string) {
 client := &http.Client{}
 req, _ := http.NewRequest("GET", url, nil)
 res, _ := client.Do(req)
 data, err := ioutil.ReadAll(res.Body)
 if err != nil {
  t.Error(err)
 }
 res.Body.Close()

  body := string(data)
 if res.StatusCode != http.StatusOK {
  t.Fatalf("Status code was wrong: %v\n %v\n %v\n", res.StatusCode, url, body)
 }
}

Load testing

Load testing is usually related to a website you made and you want to
see how fast it delivers pages.

There are lot's of ready made made tools, you don't have to make them your self.

https://github.com/lubia/sniper
https://github.com/tsenart/vegeta
https://github.com/wg/wrk
http://gatling.io/

And you can profile your program easily enough too to see where the time is going.

http://dave.cheney.net/2013/07/07/introducing-profile-super-simple-profiling-for-go-programs

Race detecting

You can also detect race conditions in Go, there is a tool for that. Consider following code:
var test = make(map[int]int)

func main() {
 var i = 0
 for i < 10000 {
  go setTest(i, i)
  fmt.Println(test[i])
  i++
 }
}

func setTest(name int, val int) {
 test[name] = val
}

You can test it with: go run -race loop.go

And you get a fatal error: concurrent map writes. This is really useful if you run your program asynchronously with goroutines.


Conclusion

I hope I had known more about testing with Go from the beginning. It's so easy to just start writing working code with Go that testing doesn't seem to be relevant. Until you make enough code to notice that everything you write doesn't actually work the way you wanted it to. Refactoring and fixing code is such a bliss when you have the tests to back it up.

Oh, I have to add this too to think about, unit tests are useful, but they are not enough.



Ei kommentteja:

Lähetä kommentti

Huomaa: vain tämän blogin jäsen voi lisätä kommentin.