Go: Testing (User + Unit Tests, QuickCheck)

Martin Sulzmann

Overview

Nobody likes to write tests! But tests are necessary. The purpose of testing. Find bugs!

We take a look at the following topics.

“Classic” methods:

“Clever” methods (such as QuickCheck):

Go testing (user + unit tests)

Counting words example

// count.go
package main

import "fmt"

// Counting words in a string.
// A word consists of a sequence of bytes separated by blanks (' ').

func skip(p func(byte) bool) func(string) string {
    return func(s string) string {
        switch {
        case len(s) == 0:
            return ""
        case p(s[0]):
            return skip(p)(s[1:])
        default:
            return s
        }
    }
}
func count(s string) int {
    skipBlanks := skip(func(b byte) bool {
        return b == ' '
    })

    skipWord := skip(func(b byte) bool {
        return b != ' '
    })
    switch {
    case len(s) == 0:
        return 0
    case s[0] == ' ':
        return count(skipBlanks(s))
    default:
        return 1 + count(skipWord(s))
    }

}

func main() {
        // Some user tests
    fmt.Printf("\n%d",count(""))
    fmt.Printf("\n%d",count("Hi df looo"))
}

Systematic testing (unit tests)

// count_test.go
package main

import "testing"

// Go testing package

func TestCount(t *testing.T) {

    var tests = []struct {
        input    string
        expected int
    }{
        {"", 0},
        {"   ", 0},
        {"Hi", 1},
        {"Hi Ho ", 2},
    }

    for _, x := range tests {
        n := count(x.input)
        if x.expected != n {
            t.Errorf("got %d, expected %d", n, x.expected)
        }

    }

}

Short summary

Writing tests by hand is painful!

QuickCheck

The QuickCheck idea:

These ideas have been around for a while. Made popular through the QuickCheck Haskell library. QuickCheck has been adopted to many other languages besides Haskell. See QuickCheck for other languages.

QuickCheck in Go

type Gen[T any] interface {
    generate() T
}

func quickCheck[T any](g Gen[T], p func(T) bool) {
    n := 100
    for i := 0; i < n; i++ {
        if p(g.generate()) {
            fmt.Println("ok")
        } else {
            fmt.Println("fail")
        }

    }

}

String generator

type Ch byte

func (x Ch) generate() byte {
    return (byte)(rand.Intn(255))
}

type S string

func (x S) generate() string {
    n := rand.Intn(15)
    var s string
    var ch Ch
    for i := 0; i < n; i++ {
        s = s + string(ch.generate())

    }
    return s
}

Generator for strings relies on a generator for byte.

Properties

func qcCount() {
    var s S
    var g Gen[string]
    g = s

    // Number of words >= 0
    p1 := func(x string) bool {
        return count(x) >= 0
    }
    quickCheck(g, p1)

    // Concatenation yields twice as many words!?
    p2 := func(x string) bool {
        return count(x+x) == 2*count(x)
    }

    quickCheck(g, p2)

}

Complete example

Summary

None of the above is meant to replace the other.

A wide range of testing methods are required.