diff --git a/mocktest/go.mod b/mocktest/go.mod new file mode 100644 index 0000000..24452a6 --- /dev/null +++ b/mocktest/go.mod @@ -0,0 +1,12 @@ +module github.com/wego/pkg/mocktest + +go 1.23.0 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/mocktest/go.sum b/mocktest/go.sum new file mode 100644 index 0000000..625f862 --- /dev/null +++ b/mocktest/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mocktest/silent.go b/mocktest/silent.go new file mode 100644 index 0000000..f3780f3 --- /dev/null +++ b/mocktest/silent.go @@ -0,0 +1,37 @@ +// Package mocktest provides small helpers for use with +// [github.com/stretchr/testify/mock] assertion helpers. +package mocktest + +// SilentT is a [github.com/stretchr/testify/mock.TestingT] implementation that +// discards every call. +// +// Use it as the TestingT argument to mock assertion helpers +// (AssertNumberOfCalls, AssertCalled, AssertNotCalled, AssertExpectations) when +// polling inside [github.com/stretchr/testify/assert.Eventually] or +// [github.com/stretchr/testify/require.Eventually], so the per-poll failure +// does not fail the outer test: +// +// assert.Eventually(t, func() bool { +// return m.AssertNumberOfCalls(mocktest.Silent, "Foo", 1) +// }, waitFor, tick) +// +// Background: testify v1.11.0 changed Eventually to evaluate the condition at +// t=0 before the first tick (stretchr/testify#1424). Passing the real +// *testing.T (or suite.T()) to a mock assertion inside Eventually causes the +// initial poll — which runs before any async goroutine has had a chance to +// fire — to call t.Errorf and permanently mark the test failed even when the +// condition becomes true on a later tick. +type SilentT struct{} + +// Logf implements [github.com/stretchr/testify/mock.TestingT] and discards the call. +func (SilentT) Logf(string, ...any) {} + +// Errorf implements [github.com/stretchr/testify/mock.TestingT] and discards the call. +func (SilentT) Errorf(string, ...any) {} + +// FailNow implements [github.com/stretchr/testify/mock.TestingT] and discards the call. +func (SilentT) FailNow() {} + +// Silent is the shared [SilentT] value. Use it as the TestingT argument to +// mock assertion helpers inside Eventually conditions. +var Silent SilentT diff --git a/mocktest/silent_test.go b/mocktest/silent_test.go new file mode 100644 index 0000000..6d10ad5 --- /dev/null +++ b/mocktest/silent_test.go @@ -0,0 +1,105 @@ +package mocktest_test + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/wego/pkg/mocktest" +) + +// SilentT must satisfy mock.TestingT at compile time. +var _ mock.TestingT = mocktest.Silent + +type fakeMock struct { + mock.Mock +} + +func (m *fakeMock) Foo() { m.Called() } + +func TestSilent_DoesNotMarkOuterTestFailed(t *testing.T) { + m := &fakeMock{} + m.On("Foo").Return() + + // Expectation is 1 but no calls have happened — would call t.Errorf + // if we passed the real t. With Silent, it is swallowed. + ok := m.AssertNumberOfCalls(mocktest.Silent, "Foo", 1) + require.False(t, ok, "expected mismatch") + require.False(t, t.Failed(), "outer test must not be marked failed") +} + +func TestSilent_WorksInsideEventually(t *testing.T) { + m := &fakeMock{} + m.On("Foo").Return() + + // Fire Foo asynchronously after a short delay so the t=0 evaluation of + // Eventually sees 0 calls. The poll must NOT fail the outer test. + go func() { + time.Sleep(20 * time.Millisecond) + m.Foo() + }() + + assert.Eventually(t, func() bool { + return m.AssertNumberOfCalls(mocktest.Silent, "Foo", 1) + }, time.Second, 5*time.Millisecond) + require.False(t, t.Failed()) +} + +func TestSilent_WorksWithAssertCalled(t *testing.T) { + m := &fakeMock{} + m.On("Foo").Return() + + // Before the goroutine fires, AssertCalled returns false but must not + // mark the outer test failed. + ok := m.AssertCalled(mocktest.Silent, "Foo") + require.False(t, ok) + require.False(t, t.Failed()) + + go func() { + time.Sleep(20 * time.Millisecond) + m.Foo() + }() + + assert.Eventually(t, func() bool { + return m.AssertCalled(mocktest.Silent, "Foo") + }, time.Second, 5*time.Millisecond) + require.False(t, t.Failed()) +} + +func TestSilent_WorksWithAssertNotCalled(t *testing.T) { + m := &fakeMock{} + m.On("Foo").Return() + + // AssertNotCalled succeeds while no call has happened. + require.True(t, m.AssertNotCalled(mocktest.Silent, "Foo")) + + // After the call, AssertNotCalled returns false but must not mark the + // outer test failed. + m.Foo() + require.False(t, m.AssertNotCalled(mocktest.Silent, "Foo")) + require.False(t, t.Failed()) +} + +func TestSilent_WorksWithAssertExpectations(t *testing.T) { + m := &fakeMock{} + m.On("Foo").Return().Once() + + var done atomic.Bool + go func() { + time.Sleep(20 * time.Millisecond) + m.Foo() + done.Store(true) + }() + + assert.Eventually(t, func() bool { + if !done.Load() { + return false + } + return m.AssertExpectations(mocktest.Silent) + }, time.Second, 5*time.Millisecond) + require.False(t, t.Failed()) +}