Alex

Testing (Video 38)

Layers of Testing

Test Functions

func TestCrypto(t *testing.T) {
  // do some tests

  if err != nil {
    t.ErrorF("failed test: %s", err)
  }
}

Subtests

func SomeTest(t *testing.T) {
  table := []struct {
    name string
    param int
    outcome int
  }{
    {name: "test1", param: 1, outcome: 10},
    {name: "test2", param: 3, outcome: 30},
  }
}

for _, st := range table {
  t.Run(st.name, func(t *testing.T) {
    // do the test here
  })
}

Nicer Subtest / Parameterised Testing Pattern

// this is the function we are testing
func someFunctionToTest(input string) bool {
  return len(input) < 5
}

// we define a named type for our parameterised test
type parameterisedTest struct {
  name  string
  input string
  want  bool
}

// we define a run method which we use as our input to t.Run
func (pt parameterisedTest) run(t *testing.T) {
  got := someFunctionToTest(pt.input)
  if pt.want != got {
    t.Errorf("input %v, wanted %v, got %v", pt.input, pt.want, got)
  }
}

// we define our table of tests
var tests = []parameterisedTest{
  {name: "happy", input: "hi", want: true},
  {name: "boundary 1", input: "hello", want: true},
  {name: "boundary 2", input: "helloo", want: false},
}

// now everything is nicely separated in our actual test and is easier to follow
func TestSomeFunctionToTest(t *testing.T) {
  for _, pt := range tests {
    t.Run(pt.name, pt.run)
  }
}

Checker Refactoring for Subtests

type checker interface {
  check(*testing.T, string, string) bool
}

type subTest struct {
  name string
  shouldFail bool
  checker checker
}

type checkGolden struct { /*...*/ }

func (c checkGolden) check(t *testing.T, got, want string) bool {
  // implement checking logic here
}

Mocking / Faking

type DB interface {
  GetThing(string) (string, error)
}

var errShouldFail = errors.New("db should fail")
type mockDB struct {
  shouldFail bool // our mock DB can have forced fail scenarios for testing
}

func (m mockDB) GetThing(key string) (string, error) {
  // mockDB now conforms to the DB interface
  if m.shouldFail {
    // we can force an error case when the flag is set
    return thing{}, fmt.Errorf("%s: %w", key, errShouldFail)
  }
}

TestMain

func TestMain(m *testing.M) {
  // setup
  setupDatabase()

  // run tests
  rc := m.Run()

  // teardown
  teardownDatabase()

  os.Exit(rc)
}

t.Cleanup

func TestSomething(t *testing.T) {
  dir := os.MkdirTemp("", "x")
  t.Cleanup(func() { os.RemoveAll(dir) }) // register cleanup function
  // test uses dir...
}
func newServer(t *testing.T) *Server {
  s := startServer()
  t.Cleanup(func(){ s.Close() }) // register cleanup in helper
  return s
}

func TestDoStuff(t *testing.T) {
  s := newServer(t)
  // use s - cleanup happens after test completes
}

Test-only Packages

Philosophy of Testing

Reproduced from https://www.youtube.com/watch?v=PIPfNIWVbc8
Reproduced from https://www.youtube.com/watch?v=PIPfNIWVbc8