Github.com/stretchr/testify: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(93 intermediate revisions by the same user not shown)
Line 4: Line 4:
=Internal=
=Internal=
* [[Go_Testing#Helper_Packages|Go Testing]]
* [[Go_Testing#Helper_Packages|Go Testing]]
* [[Go_test_Command#Overview|<tt>go test</tt>]]
=TODO=
<font color=darkkhaki>
TO PROCESS:
* https://tychoish.com/post/against-testify/
</font>
=Overview=
=Overview=
Testify wraps around and delegates to the standard <code>[[Go_Testing#Overview|testing]]</code> package.
=Installation=
=Installation=
<syntaxhighlight lang='bash'>
<syntaxhighlight lang='bash'>
Line 10: Line 20:
</syntaxhighlight>
</syntaxhighlight>
=Programming Model=
=Programming Model=
==<tt>require</tt> and <tt>assert</tt>==
{{Internal|Testify require and assert#Overview|Testify <tt>require</tt> and <tt>assert</tt>}}
==Mocks==
{{External|https://pkg.go.dev/github.com/stretchr/testify/mock}}
The Testify <code>mock</code> package provides a system by which it is possible to mock objects and verify calls into those objects are happening as expected. Mocking with Testify is based on the assumption that we want to construct [[Software_Testing_Concepts#Mock|mock instances]] to replace in testing [[Go_Interfaces#Overview|interface-defined]] real instances. Mocks can stand in for [[Software_Testing_Concepts#External_Dependency|external dependencies]] or other complex components defined by their interfaces.
The programming model requires to:
* [[#Start_with_an_Interface|start with an interface]], which will be implemented by the mock
* [[#Defining_the_Mock|define]] the mock structure
* [[#Instantiate_the_Mock|instantiate]] it with <code>new()</code> or <code>&SomeInterfaceMock{}</code>
* [[#Set_Up_Expectations|configure its behavior]], by configuring its methods' responses to invocations
* [[#Test_with_the_Mock_and_Verify_the_Results|pass]] it to the code that needs to be tested
* [[#Test_with_the_Mock_and_Verify_the_Results|run]] the code that needs to be tested
* [[#Test_with_the_Mock_and_Verify_the_Results|ensure]] the code behave correctly, knowing that the mock returned what we instructed it to return
* optionally, [[#Assert_Expectations|assert expectations]] on the mock: where the methods called?
===Start with an Interface===
Let's assume that the instances we want to mock are defined by the <code>Something</code> interface, declared as follows:
<syntaxhighlight lang='go'>
package somepkg
type Something interface {
SomeFunc(s string, i int) (string, error)
SomeOtherFunc(s string) (string, error)
}
</syntaxhighlight>
Our mocks will implement that interface and respond to invocations into it.
===<span id='Defining_the_Mock'></span>Define the Mock===
It's a good idea to encapsulate the mock definition(s) in a package-level <code>mocks_test.go</code> file. If we're testing a <code>somepkg</code> package, then the code lives in the <code>somepkg.go</code> file, the tests live in <code>somepkg_test.go</code> file and the mocks live in <code>mocks_test.go</code>. Note that declaring the mock in a file whose name ends in "_test.go" prevents it from being consumed in other packages, but that should not be a problem. The mocks should be defined in the same package that consumes them for testing, and if you are finding in the position of not being able to consume the mocks, you're doing something wrong. For the same reasons, the type names for mocks and their constructors show always start with a lower case: <code>mockSomething</code>, <code>newMockSomething</code>.
<font size=-2>
.
└── internal
     └── somepkg
        ├── somepkg.go
       ├── somepkg_test.go
       └── mocks_test.go
</font>
The mock, implemented as the <code>mockSomething</code> <code>struct</code>, should be defined in the <code>mocks_test.go</code> file.
<span id='Build_Tags'></span>Optionally, add [[Go_Build_Tags#Controlling_Test_Execution_with_Build_Tags|build tags]] for the type of tests the mocks will be used with:
<syntaxhighlight lang='go'>
//go:build unit_test || integration_test
package somepkg
[...]
</syntaxhighlight>
The mock struct should be declared as a wrapper around the Testify <code>mock.Mock</code> struct, which provides all functionality required to tracks activity on the actual mock object. Aside wrapping around <code>mock.Mock</code>, the struct may declare other fields, as needed. It is common to provide simple implementations to some of the mocked methods that rely on those fields. <code>mockSomething</code> should implement all the methods that are going to be used in testing. Implementation examples are provided below. In the generic case, the method implementation should forward the invocation to the internal mock instance with <code>Called(args)</code> and return what the mock returns as result of <code>Called()</code>. In other specific cases, we may come up with our simple implementation. If we know for sure that a method will not be exercised in testing, it is fine to let it <code>panic("not yet implemented")</code>.
<syntaxhighlight lang='go'>
//go:build unit_test || integration_test
package somepkg
import "github.com/stretchr/testify/mock"
type mockSomething struct {
mock.Mock
    // other fields may be added here
}
// Something interface implementation
func (s *mockSomething) SomeFunc(sa string, i int) (string, error) {
r := s.Called(sa, i)
return r.String(0), r.Error(1)
}
func (s *mockSomething) SomeOtherFunc(sa string) (string, error) {
panic("not yet implemented")
}
</syntaxhighlight>
For arbitrary type objects, use <code>Arguments.Get(index)</code> and make a type assertion. This may cause a panic if the object you are getting is <code>nil</code>, the type assertion will fail, and in those cases you should check for <code>nil</code> first:
<syntaxhighlight lang='go'>
func (s *mockSomething) SomeFunc2(...) (*SomeType, *SomeOtherType) {
r := s.Called(sa, i)
return r.Get(0).(*SomeType), r.Get(1).(* SomeOtherType)
}
</syntaxhighlight>
===Instantiate the Mock===
Instantiate the mock in the testing code with <code>new()</code> or <code>&mockSomething{}</code>:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
package yours


func TestSomething(t *testing.T) {
mock := new(mockSomething)
// or mock := &mockSomething{}
    [...]
}
</syntaxhighlight>
===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".
====Defining the Behavior of a Method====
The <code>On()</code> method may be used to configure the mock to respond in a certain way to a method invocation. If <code>On()</code> is used to set behavior, then the mock can then be queried to assert whether the call actually happened or not, with <code>[[#Assert_Expectations|AssertExpectations()]]</code>.
<syntaxhighlight lang='go'>
// On(function_name, arguments ...).Return(return_value_1, return_value_2, ...)
mock.On("SomeFunc", "fish", 3).Return("bouillabaisse", nil)
</syntaxhighlight>
When the function is invoked with arguments different than the ones it was configured with, the mock with panic with:
<font size=-1>
mock: Unexpected Method Call
</font>
We can use specific argument values when configuring the method behavior, like in the example above, or we can use placeholders (<code>mock.Anything</code>) for each argument where the data being passed in is dynamically generated and cannot be predicted beforehand:
<syntaxhighlight lang='go'>
import (
import (
  "testing"
"testing"
  "github.com/stretchr/testify/assert"
 
testifymock "github.com/stretchr/testify/mock"
)
)


func TestSomething(t *testing.T) {
[...]
 
mock.On("SomeFunc", testifymock.Anything, testifymock.Anything).Return("bouillabaisse", nil)
</syntaxhighlight>
 
{{Warn|It is important to use the same number of arguments as in the method signature when configuring the mock with <code>On()</code> and <code>Anything</code>}}
 
===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.
 
<syntaxhighlight lang='go'>
// this is the code to be tested
func Usage(si Something, s string, i int) (string, error) {
return si.SomeFunc(s, i)
}
</syntaxhighlight>
 
This is the test:
<syntaxhighlight lang='go'>
func TestUsage(t *testing.T) {
assert := tassert.New(t)
mock := new(mockSomething)
mock.On("SomeFunc", "fish", 3).Return("bouillabaisse", nil)
sr, err := Usage(mock, "fish", 3)
assert.Equal("bouillabaisse", sr)
assert.Nil(err)
}
</syntaxhighlight>


  assert := assert.New(t)
===Assert Expectations===
<syntaxhighlight lang='go'>
func TestUsage(t *testing.T) {


  // assert equality
[...]
  assert.Equal(123, 123, "they should be equal")
    mock.AssertExpectations(t)
}
</syntaxhighlight>


  // assert inequality
<code>AssertExpectations</code> asserts that everything specified with <code>On()</code> and <code>Return</code> was in fact called as expected. Calls may have occurred in any order.
  assert.NotEqual(123, 456, "they should not be equal")


  // assert for nil (good for errors)
Other method that can be used to assert invocations with a finer granularity:
  assert.Nil(object)
<syntaxhighlight lang='go'>
mock.AssertCalled(t, methodName string, arguments ...interface{})
mock.AssertNotCalled(t, methodName string, arguments ...interface{})
mock.AssertNumberOfCalls(t, methodName string, expectedCalls int)
mock.Called(...)
mock.IsMethodCallable(...)
mock.MethodCalled(...)
</syntaxhighlight>


  // assert for not nil (good when you expect something)
===Reconfigure===
  if assert.NotNil(object) {
The mock instances can be "unconfigured" with <code>Unset()</code> invoked on the result of <code>On()</code> and then reconfigured again with <code>On</code>:
<syntaxhighlight lang='go'>
func TestUsage(t *testing.T) {


     // now we know that object isn't nil, we are safe to make
    mockCall := mock.On("SomeFunc", ....)
     // further assertions without causing any errors
     // test
     assert.Equal("Something", object.Value)
    [...]   
  }
    mock.AssertExpectations(t)
    // unset
    mockCall.Unset()
     // reconfigure
     mockCall := mock.On("SomeFunc", ....)
    // test
    [...]
}
}
</syntaxhighlight>
</syntaxhighlight>

Latest revision as of 00:34, 13 March 2024

External

Internal

TODO

TO PROCESS:

Overview

Testify wraps around and delegates to the standard testing package.

Installation

go get github.com/stretchr/testify

Programming Model

require and assert

Testify require and assert

Mocks

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

The Testify mock package provides a system by which it is possible to mock objects and verify calls into those objects are happening as expected. Mocking with Testify is based on the assumption that we want to construct mock instances to replace in testing interface-defined real instances. Mocks can stand in for external dependencies or other complex components defined by their interfaces.

The programming model requires to:

Start with an Interface

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)
}

Our mocks will implement that interface and respond to invocations into it.

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 mocks_test.go. Note that declaring the mock in a file whose name ends in "_test.go" prevents it from being consumed in other packages, but that should not be a problem. The mocks should be defined in the same package that consumes them for testing, and if you are finding in the position of not being able to consume the mocks, you're doing something wrong. For the same reasons, the type names for mocks and their constructors show always start with a lower case: mockSomething, newMockSomething.

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

The mock, implemented as the mockSomething struct, should be defined in the mocks_test.go file.

Optionally, add build tags for the type of tests the mocks will be used with:

//go:build unit_test || integration_test

package somepkg

[...]

The mock struct should be declared as a wrapper around the Testify mock.Mock struct, which provides all functionality required to tracks activity on the actual mock object. Aside wrapping around mock.Mock, the struct may declare other fields, as needed. It is common to provide simple implementations to some of the mocked methods that rely on those fields. mockSomething should implement all the methods that are going to be used in testing. Implementation examples are provided below. In the generic case, 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(). In other specific cases, we may come up with our simple implementation. If we know for sure that a method will not be exercised in testing, it is fine to let it panic("not yet implemented").

//go:build unit_test || integration_test

package somepkg

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

type mockSomething struct {
	mock.Mock
    // other fields may be added here
}

// Something interface implementation

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

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

For arbitrary type objects, use Arguments.Get(index) and make a type assertion. This may cause a panic if the object you are getting is nil, the type assertion will fail, and in those cases you should check for nil first:

func (s *mockSomething) SomeFunc2(...) (*SomeType, *SomeOtherType) {
	r := s.Called(sa, i)
	return r.Get(0).(*SomeType), r.Get(1).(* SomeOtherType)
}

Instantiate the Mock

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

func TestSomething(t *testing.T) {

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

    [...]
}

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".

Defining the Behavior of a Method

The On() method may be used to configure the mock to respond in a certain way to a method invocation. If On() is used to set behavior, then the mock can then be queried to assert whether the call actually happened or not, with AssertExpectations().

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

When the function is invoked with arguments different than the ones it was configured with, the mock with panic with:

mock: Unexpected Method Call

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

import (
	"testing"

	testifymock "github.com/stretchr/testify/mock"
)

[...]

mock.On("SomeFunc", testifymock.Anything, testifymock.Anything).Return("bouillabaisse", nil)

It is important to use the same number of arguments as in the method signature when configuring the mock with On() and Anything

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(mockSomething)
	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)
}

AssertExpectations asserts that everything specified with On() and Return was in fact called as expected. Calls may have occurred in any order.

Other method that can be used to assert invocations with a finer granularity:

mock.AssertCalled(t, methodName string, arguments ...interface{})
mock.AssertNotCalled(t, methodName string, arguments ...interface{})
mock.AssertNumberOfCalls(t, methodName string, expectedCalls int)
mock.Called(...)
mock.IsMethodCallable(...)
mock.MethodCalled(...)

Reconfigure

The mock instances can be "unconfigured" with Unset() invoked on the result of On() and then reconfigured again with On:

func TestUsage(t *testing.T) {

    mockCall := mock.On("SomeFunc", ....)
    // test
    [...]    
    mock.AssertExpectations(t)
    // unset
    mockCall.Unset()
    // reconfigure 
    mockCall := mock.On("SomeFunc", ....)
    // test
    [...]
}