mock: add negative call expectations
The the moment once cannot express negative expectations via .On(). Instead, you must use .AssertNotCalled. It would be useful if negative expectations could be expressed something like .On().Never(). Note that .On().Times(0) does not work.
We could use AssertNotCalled. Is there any other specific reason or thoughts that you want such feature. Times(0) doesn't work, because closest call checks for -1.
if call.Method == method && call.Repeatability > -1 {
we could make the Times panic or fail if its passed with an argument <= 0
Even if we set expectation like mock.Never("method") before the actual call of function and it panics during actual call, I still feel that both would be the same feature wise.
@eliaslevy I wish Times(0) worked as you would expect. Unfortunately, it is a breaking change that requires a new major version for testify.
You can do Times(-1).
I wouldn't be opposed to adding .Never() which sets Repeatability to -1.
I don't think Never() would be very useful, it's almost the same as not defining a call at all.
Right now in v1.6.1 Times(-1) does not work, it fails expectations whether the call is made or not:
package kata
import (
"testing"
"github.com/stretchr/testify/mock"
)
type M struct {
mock.Mock
}
func (m *M) Do() {
m.Called()
}
func TestMockNotCalled(t *testing.T) {
m := &M{}
m.Test(t)
defer m.AssertExpectations(t)
m.On("Do").Return().Times(-1)
}
func TestMockCalled(t *testing.T) {
m := &M{}
m.Test(t)
defer m.AssertExpectations(t)
m.On("Do").Return().Times(-1)
m.Do()
}
Running tool: /usr/local/go/bin/go test -timeout 30s -coverprofile=/var/folders/8x/rpf58y2d68z45w0b5_mg47b00000gn/T/vscode-goCmRNOx/go-code-cover github.com/brackendawson/kata
--- FAIL: TestMockNotCalled (0.00s)
kata_test.go:22: FAIL: Do()
at: [kata_test.go:21]
kata_test.go:22: FAIL: 0 out of 1 expectation(s) were met.
The code you are testing needs to make 1 more call(s).
at: [kata_test.go:22]
--- FAIL: TestMockCalled (0.00s)
mock.go:256:
assert: mock: The method has been called over 0 times.
Either do one more Mock.On("Do").Return(...), or remove extra call.
This call was unexpected:
Do()
at: [kata_test.go:14 kata_test.go:29]
panic.go:617: FAIL: Do()
at: [kata_test.go:28]
panic.go:617: FAIL: 0 out of 1 expectation(s) were met.
The code you are testing needs to make 1 more call(s).
at: [panic.go:617 testing.go:657 kata_test.go:14 kata_test.go:29]
FAIL
coverage: [no statements]
FAIL github.com/brackendawson/kata 0.198s
FAIL
My preference would be that Times(0) just do what you expect.
I wanted to add a possible use case here. In a table driven test, you might want to provide the number of times you expect a mocked method to be called. Sometimes, you may want a certain mocked method to not be called. In this case, you have to add an if statement in you test to check if the expected number of calls is >0, is so, setup your call assertions. A novel example:
package metricsmiddleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/mymodule/mocks"
)
func TestMetricsMiddleware(t *testing.T) {
table := []struct {
name string
status int
expectedLatency time.Duration
expected4XXCalls int
expected5XXCalls int
}{
{
name: "it should send a latency metric with the expected duration",
status: 200,
expectedLatency: time.Millisecond,
expected4XXCalls: 0,
expected5XXCalls: 0,
},
{
name: "it should send a 4xx error metric when the response status code is in the 400 range",
status: 422,
expectedLatency: time.Millisecond,
expected4XXCalls: 1,
expected5XXCalls: 0,
},
{
name: "it should send a 5xx error metric when the response status code is in the 500 range",
status: 500,
expectedLatency: time.Millisecond,
expected4XXCalls: 1,
expected5XXCalls: 0,
},
}
for _, test := range table {
t.Run(test.name, func(t *testing.T) {
mockMetrics := &mocks.Metrics{}
mockMetrics.On("Timing", "latency", test.expectedLatency, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
mockMetrics.On("Incr", "error_4xx", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return().Times(test.expected4XXCalls)
mockMetrics.On("Incr", "error_5xx", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return().Times(test.expected5XXCalls)
mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.status)
})
mw := middleware.NewMetricsMiddleware(mockMetrics)(mockHandler)
mw.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/foo", nil))
mockMetrics.AssertExpectations(t)
})
}
}
The first test in the table won't pass with the current state, as 0 is passed to Times().