Github.com/stretchr/testify

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

Overview

Installation

go get github.com/stretchr/testify

Programming Model

Assertions

package yours

import (
  "testing"
  tassert "github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {

  assert := tassert.New(t)

  // assert equality
  assert.Equal(123, 123, "they should be equal")

  // assert inequality
  assert.NotEqual(123, 456, "they should not be equal")

  // assert for nil (good for errors)
  assert.Nil(object)

  // assert for not nil (good when you expect something)
  if assert.NotNil(object) {

    // now we know that object isn't nil, we are safe to make
    // further assertions without causing any errors
    assert.Equal("Something", object.Value)
  }
}

To check that an error has the expected message:

err := ...
assert.NotNil(err)
assert.Equal(err.Error(), "expected message")

Mocks

https://pkg.go.dev/github.com/stretchr/testify/mock

Mocking with Testify is based on the assumption that we want to construct mock instances to replace in testing real instances, standing in for external dependencies, defined by interfaces.

The programming model requires to:

  • define such a mock instance
  • instantiate it with new() or &SomeInterfaceMock{}
  • configure its behavior, by configuring its methods' responses to invocations
  • pass it to the code that needs to be tested
  • run the code that needs to be tested
  • ensure the code behave correctly, knowing that the mock returned what we instructed it to return
  • optionally, assert expectations on the mock.

Define the Mock

It's a good idea to encapsulate the mock definition(s) in a package-level *_mocks_test.go file. If we're testing a somepkg package, then the code lives in the somepkg.go file, the tests live in somepkg_test.go file and the mocks live in somepkg_mocks_test.go:

.
└── internal
    └── somepkg
        ├── somepkg.go
        ├── somepkg_test.go
        └── somepkg_mocks_test.go

Let's assume that the instances we want to mock are defined by the Something interface, declared as follows:

package somepkg

type Something interface {
	SomeFunc(s string, i int) (string, error)
	SomeOtherFunc(s string) (string, error)
}

The mock, implemented as the SomethingMock struct, should be defined in the somepkg_mocks_test.go file. The mock struct is a wrapper around the Testify mock.Mock structure, which provides all functionality required by mocking.

SomethingMock should implement all the methods that are going to be used in testing. Implementation examples are provided below: the method implementation should forward the invocation to the internal mock instance with Called(args) and return what the mock returns as result of Called(). If we know for sure that a method will not be exercised in testing, it is fine to let it panic("not yet implemented").

package somepkg

import "github.com/stretchr/testify/mock"

type SomethingMock struct {
	mock.Mock
}

// Something interface implementation

func (s *SomethingMock) SomeFunc(sa string, i int) (string, error) {
	r := s.Called(sa, i)
	return r.String(0), r.Error(1)
}

func (s *SomethingMock) SomeOtherFunc(sa string) (string, error) {
	panic("not yet implemented")
}

Instantiate the Mock

Instantiate the mock in the testing code with new() or &SomethingMock{}:

func TestSomething(t *testing.T) {

	mock := new(SomethingMock)
	// or mock := &SomethingMock{}

    [...]
}

Set Up Expectations

After instantiation, we configure the mock's behavior by configuring its methods' responses to invocations. Testify calls this stage "setting up the expectations".

[...]

// On(function_name, arguments ...).Return(return_value_1, return_value_2, ...)
mock.On("SomeFunc", "fish", 3).Return("bouillabaisse ", nil)

[...]

We can use specific argument values when configuring the method behavior, like in the example above, or we can use placeholders (mock.Anything) when the data being passed in is dynamically generated and cannot be predicted beforehand.

Test with the Mock and Verify the Results

Pass the mock to the code that needs to be tested, run the code and ensure it behaves correctly, knowing that the mock returnes what we instructed it to return.

// this is the code to be tested
func Usage(si Something, s string, i int) (string, error) {
	return si.SomeFunc(s, i)
}

This is the test:

func TestUsage(t *testing.T) {
	assert := tassert.New(t)
	mock := new(SomethingMock)
	mock.On("SomeFunc", "fish", 3).Return("bouillabaisse", nil)
	sr, err := Usage(mock, "fish", 3)
	assert.Equal("bouillabaisse", sr)
	assert.Nil(err)
}

Assert Expectations

func TestUsage(t *testing.T) {

	[...]
    mock.AssertExpectations(t)
}