Compare commits

...

16 Commits

Author SHA1 Message Date
Bracken
e8daaaca9c
Merge pull request #1665 from vyas-git/master
suite: validate method signatures and continue execution for valid tests
2025-08-29 11:16:46 +02:00
Vyas Reddy
96f97a2639
Merge branch 'master' into master 2025-08-28 23:36:22 +05:30
Vyas Reddy
5f941c88ce
Update suite/suite.go
Co-authored-by: Bracken <abdawson@gmail.com>
2025-08-28 23:36:11 +05:30
Bracken
c952903e9f
Merge pull request #1786 from brackendawson/1785-mock-argument-stringers
mock: revert to pre-v1.11.0 argument matching behavior for mutating stringers
2025-08-27 12:31:33 +02:00
Bracken Dawson
0cdb408f53
Revert "Merge pull request #1615 from DevotedHealth/mauclair-mock-match-sprintf"
This reverts commit a31a53e5b43e2c8827f61e9a13e885c64b754805, reversing
changes made to 5ac6528bffc1ed7557980c3c563caf8308568446.
2025-08-27 11:50:26 +02:00
Bracken Dawson
5e25bfb162
mock: Test that arguments with mutating stringers match
Covers the use case in issue 1785 where mock can fail to match arguments by inappropriately mutating them by calling their stringer interface.
2025-08-27 11:37:25 +02:00
Bracken
f4f5960503
Merge pull request #1783 from stretchr/ci-go1.23
CI: test also with Go 1.23
2025-08-25 16:12:46 +02:00
Olivier Mengué
af16170941 CI: test also with Go 1.23
Now that Go 1.25.0 is released, Go 1.23 must be explicitely mentioned
for testing as it was previously "oldstable".
2025-08-25 16:05:54 +02:00
Bracken
e581b36d7b
Merge pull request #1775 from mutaiib/bugfix/panic-methodcalled-mutex-release-1731
mock: avoid panic when expected type is nil in Arguments.Diff
2025-08-18 21:30:04 +02:00
mutaiib
e3d64ad5af
Update mock/mock_test.go
Depending on the version of go used, context.Background returns either a context.emptyCtx or a context.Backgound since they made it into its own type. This is why the tests will fail for Go 1.18.

Co-authored-by: Bracken <abdawson@gmail.com>
2025-08-19 00:19:15 +05:30
mutaiib
354fc33d53 Update mock/mock.go
In short, Mock cannot match interface types with `IsType`, it only matches the values' concrete type.

Co-authored-by: Bracken <abdawson@gmail.com>
2025-08-18 21:38:15 +05:30
mutaiib
8976267da7 mock: document IsType interface limitation and add tests
- Updated IsType doc comment to clarify behavior with interface types:
  - IsType matches the exact concrete type provided.
  - Passing a nil interface has no type information and will never match.
  - To match interface values, a non-nil concrete value must be provided.
- Added tests for interface type behavior:
  - Test_Arguments_Diff_WithIsTypeArgument_InterfaceType (non-nil, matches)
  - Test_Arguments_Diff_WithIsTypeArgument_InterfaceType_Failing (nil, mismatch)
2025-08-18 21:38:15 +05:30
mutaiib
b9a23f4b6a refactor: unexport and convert Arguments.SafeName method to standalone function
- Renamed `Arguments.SafeName` to `safeTypeName`
2025-08-18 21:38:15 +05:30
mutaiib
7b2204f8a3 fix(pkg-mock): avoid panic when expected type is nil in Arguments.Diff
Calling Name() on a nil reflect.Type would panic (and leave the mutex locked),
causing hard‑to‑diagnose crashes.
Introduce a SafeName helper that returns
"<nil>" for nil types (or t.Name() otherwise), and switch the Diff method to
use SafeName(expected.t) instead of expected.t.Name() when rendering failures.
2025-08-18 21:38:15 +05:30
Vyas Reddy
fada016416
Merge branch 'master' into master 2025-07-04 20:36:22 +05:30
vyas-git
fac5d473cb invalid method test signatures fails as subtest, other continues 2025-06-24 09:01:02 +05:30
5 changed files with 146 additions and 56 deletions

View File

@ -32,6 +32,7 @@ jobs:
- "1.20"
- "1.21"
- "1.22"
- "1.23"
steps:
- uses: actions/checkout@v5
- name: Setup Go

View File

@ -833,6 +833,10 @@ type IsTypeArgument struct {
// For example:
//
// args.Assert(t, IsType(""), IsType(0))
//
// Mock cannot match interface types because the contained type will be passed
// to both IsType and Mock.Called, for the zero value of all interfaces this
// will be <nil> type.
func IsType(t interface{}) *IsTypeArgument {
return &IsTypeArgument{t: reflect.TypeOf(t)}
}
@ -948,8 +952,6 @@ func (args Arguments) Is(objects ...interface{}) bool {
return true
}
type outputRenderer func() string
// Diff gets a string describing the differences between the arguments
// and the specified objects.
//
@ -957,7 +959,7 @@ type outputRenderer func() string
func (args Arguments) Diff(objects []interface{}) (string, int) {
// TODO: could return string as error and nil for No difference
var outputBuilder strings.Builder
output := "\n"
var differences int
maxArgCount := len(args)
@ -965,35 +967,24 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
maxArgCount = len(objects)
}
outputRenderers := []outputRenderer{}
for i := 0; i < maxArgCount; i++ {
i := i
var actual, expected interface{}
var actualFmt, expectedFmt func() string
var actualFmt, expectedFmt string
if len(objects) <= i {
actual = "(Missing)"
actualFmt = func() string {
return "(Missing)"
}
actualFmt = "(Missing)"
} else {
actual = objects[i]
actualFmt = func() string {
return fmt.Sprintf("(%[1]T=%[1]v)", actual)
}
actualFmt = fmt.Sprintf("(%[1]T=%[1]v)", actual)
}
if len(args) <= i {
expected = "(Missing)"
expectedFmt = func() string {
return "(Missing)"
}
expectedFmt = "(Missing)"
} else {
expected = args[i]
expectedFmt = func() string {
return fmt.Sprintf("(%[1]T=%[1]v)", expected)
}
expectedFmt = fmt.Sprintf("(%[1]T=%[1]v)", expected)
}
if matcher, ok := expected.(argumentMatcher); ok {
@ -1001,22 +992,16 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
func() {
defer func() {
if r := recover(); r != nil {
actualFmt = func() string {
return fmt.Sprintf("panic in argument matcher: %v", r)
}
actualFmt = fmt.Sprintf("panic in argument matcher: %v", r)
}
}()
matches = matcher.Matches(actual)
}()
if matches {
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: PASS: %s matched by %s\n", i, actualFmt(), matcher)
})
output = fmt.Sprintf("%s\t%d: PASS: %s matched by %s\n", output, i, actualFmt, matcher)
} else {
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: %s not matched by %s\n", i, actualFmt(), matcher)
})
output = fmt.Sprintf("%s\t%d: FAIL: %s not matched by %s\n", output, i, actualFmt, matcher)
}
} else {
switch expected := expected.(type) {
@ -1025,17 +1010,13 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
if reflect.TypeOf(actual).Name() != string(expected) && reflect.TypeOf(actual).String() != string(expected) {
// not match
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: type %s != type %s - %s\n", i, expected, reflect.TypeOf(actual).Name(), actualFmt())
})
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, expected, reflect.TypeOf(actual).Name(), actualFmt)
}
case *IsTypeArgument:
actualT := reflect.TypeOf(actual)
if actualT != expected.t {
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: type %s != type %s - %s\n", i, expected.t.Name(), actualT.Name(), actualFmt())
})
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, safeTypeName(expected.t), safeTypeName(actualT), actualFmt)
}
case *FunctionalOptionsArgument:
var name string
@ -1046,36 +1027,26 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
const tName = "[]interface{}"
if name != reflect.TypeOf(actual).String() && len(expected.values) != 0 {
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: type %s != type %s - %s\n", i, tName, reflect.TypeOf(actual).Name(), actualFmt())
})
output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, tName, reflect.TypeOf(actual).Name(), actualFmt)
} else {
if ef, af := assertOpts(expected.values, actual); ef == "" && af == "" {
// match
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: PASS: %s == %s\n", i, tName, tName)
})
output = fmt.Sprintf("%s\t%d: PASS: %s == %s\n", output, i, tName, tName)
} else {
// not match
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: %s != %s\n", i, af, ef)
})
output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, af, ef)
}
}
default:
if assert.ObjectsAreEqual(expected, Anything) || assert.ObjectsAreEqual(actual, Anything) || assert.ObjectsAreEqual(actual, expected) {
// match
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: PASS: %s == %s\n", i, actualFmt(), expectedFmt())
})
output = fmt.Sprintf("%s\t%d: PASS: %s == %s\n", output, i, actualFmt, expectedFmt)
} else {
// not match
differences++
outputRenderers = append(outputRenderers, func() string {
return fmt.Sprintf("\t%d: FAIL: %s != %s\n", i, actualFmt(), expectedFmt())
})
output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, actualFmt, expectedFmt)
}
}
}
@ -1086,12 +1057,7 @@ func (args Arguments) Diff(objects []interface{}) (string, int) {
return "No differences.", differences
}
outputBuilder.WriteString("\n")
for _, r := range outputRenderers {
outputBuilder.WriteString(r())
}
return outputBuilder.String(), differences
return output, differences
}
// Assert compares the arguments with the specified objects and fails if
@ -1179,6 +1145,15 @@ func (args Arguments) Bool(index int) bool {
return s
}
// safeTypeName returns the reflect.Type's name without causing a panic.
// If the provided reflect.Type is nil, it returns the placeholder string "<nil>"
func safeTypeName(t reflect.Type) string {
if t == nil {
return "<nil>"
}
return t.Name()
}
func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) {
t := reflect.TypeOf(v)
k := t.Kind()

View File

@ -1,10 +1,12 @@
package mock
import (
"context"
"errors"
"fmt"
"regexp"
"runtime"
"strconv"
"sync"
"testing"
"time"
@ -1998,6 +2000,25 @@ func Test_Arguments_Diff_WithIsTypeArgument_Failing(t *testing.T) {
assert.Contains(t, diff, `string != type int - (int=123)`)
}
func Test_Arguments_Diff_WithIsTypeArgument_InterfaceType(t *testing.T) {
t.Parallel()
var ctx = context.Background()
args := Arguments([]interface{}{IsType(ctx)})
_, count := args.Diff([]interface{}{context.Background()})
assert.Equal(t, 0, count)
}
func Test_Arguments_Diff_WithIsTypeArgument_InterfaceType_Failing(t *testing.T) {
t.Parallel()
var ctx context.Context
var args = Arguments([]interface{}{IsType(ctx)})
diff, count := args.Diff([]interface{}{context.Background()})
assert.Equal(t, 1, count)
assert.Contains(t, diff, `type <nil> != type `)
}
func Test_Arguments_Diff_WithArgMatcher(t *testing.T) {
t.Parallel()
@ -2421,3 +2442,22 @@ type user interface {
type mockUser struct{ Mock }
func (m *mockUser) Use(c caller) { m.Called(c) }
type mutatingStringer struct {
N int
s string
}
func (m *mutatingStringer) String() string {
m.s = strconv.Itoa(m.N)
return m.s
}
func TestIssue1785ArgumentWithMutatingStringer(t *testing.T) {
m := &Mock{}
m.On("Method", &mutatingStringer{N: 2})
m.On("Method", &mutatingStringer{N: 1})
m.MethodCalled("Method", &mutatingStringer{N: 1})
m.MethodCalled("Method", &mutatingStringer{N: 2})
m.AssertExpectations(t)
}

View File

@ -158,7 +158,19 @@ func Run(t *testing.T, suite TestingSuite) {
if matchMethodRE != nil && !matchMethodRE.MatchString(method.Name) {
continue
}
// Check method signature
if method.Type.NumIn() > 1 || method.Type.NumOut() > 0 {
tests = append(tests, test{
name: method.Name,
run: func(t *testing.T) {
t.Errorf(
"testify: suite method %q has invalid signature: expected no input or output parameters, method has %d input parameters and %d output parameters",
method.Name, method.Type.NumIn()-1, method.Type.NumOut(),
)
},
})
continue
}
test := test{
name: method.Name,
run: func(t *testing.T) {

View File

@ -751,3 +751,65 @@ func TestUnInitializedSuites(t *testing.T) {
})
})
}
// SuiteSignatureValidationTester tests valid and invalid method signatures.
type SuiteSignatureValidationTester struct {
Suite
executedTestCount int
setUp bool
toreDown bool
}
// SetupSuite runs once before any tests.
func (s *SuiteSignatureValidationTester) SetupSuite() {
s.setUp = true
}
// TearDownSuite runs once after all tests.
func (s *SuiteSignatureValidationTester) TearDownSuite() {
s.toreDown = true
}
// Valid test method — should run.
func (s *SuiteSignatureValidationTester) TestValidSignature() {
s.executedTestCount++
}
// Invalid: has return value.
func (s *SuiteSignatureValidationTester) TestInvalidSignatureReturnValue() interface{} {
s.executedTestCount++
return nil
}
// Invalid: has input arg.
func (s *SuiteSignatureValidationTester) TestInvalidSignatureArg(somearg string) {
s.executedTestCount++
}
// Invalid: both input arg and return value.
func (s *SuiteSignatureValidationTester) TestInvalidSignatureBoth(somearg string) interface{} {
s.executedTestCount++
return nil
}
// TestSuiteSignatureValidation ensures that invalid signature methods fail and valid method runs.
func TestSuiteSignatureValidation(t *testing.T) {
suiteTester := new(SuiteSignatureValidationTester)
ok := testing.RunTests(allTestsFilter, []testing.InternalTest{
{
Name: "signature validation",
F: func(t *testing.T) {
Run(t, suiteTester)
},
},
})
require.False(t, ok, "Suite should fail due to invalid method signatures")
assert.Equal(t, 1, suiteTester.executedTestCount, "Only the valid test method should have been executed")
assert.True(t, suiteTester.setUp, "SetupSuite should have been executed")
assert.True(t, suiteTester.toreDown, "TearDownSuite should have been executed")
}