testify icon indicating copy to clipboard operation
testify copied to clipboard

Negative expectations

Open eliaslevy opened this issue 6 years ago • 4 comments

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.

eliaslevy avatar Mar 01 '18 00:03 eliaslevy

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.

devdinu avatar Mar 15 '18 14:03 devdinu

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

ernesto-jimenez avatar Mar 18 '18 19:03 ernesto-jimenez

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.

brackendawson avatar Jun 20 '20 16:06 brackendawson

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().

jcass8695 avatar Aug 09 '22 10:08 jcass8695