diff --git a/.ci.readme.fmt.sh b/.ci.readme.fmt.sh new file mode 100755 index 0000000..045cb5f --- /dev/null +++ b/.ci.readme.fmt.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Verify that the code snippets in README.md are formatted. +# The tool https://github.com/hougesen/mdsf is used. + +if [ -n "$(mdsf verify --config .mdsf.json --log-level error README.md 2>&1)" ]; then + echo "Go code in the README.md is not formatted." + echo "Did you forget to run 'mdsf format --config .mdsf.json README.md'?" + mdsf format --config .mdsf.json README.md + git diff + exit 1 +fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1dd4e65..2aed145 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,8 +15,10 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ matrix.go_version }} + - run: npm install -g mdsf-cli - run: ./.ci.gogenerate.sh - run: ./.ci.gofmt.sh + - run: ./.ci.readme.fmt.sh - run: ./.ci.govet.sh - run: go test -v -race ./... test: diff --git a/.mdsf.json b/.mdsf.json new file mode 100644 index 0000000..884bbbe --- /dev/null +++ b/.mdsf.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/hougesen/mdsf/main/schemas/v0.8.2/mdsf.schema.json", + "format_finished_document": false, + "languages": { + "go": [ + [ + "gofmt", + "goimports" + ] + ] + } +} diff --git a/MAINTAINERS.md b/MAINTAINERS.md index cb9a39a..120c463 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -6,5 +6,12 @@ pull requests. * @boyan-soubachov * @dolmen * @MovieStoreGuy - * @arjunmahishi * @brackendawson + +## Approvers + +The individuals listed below are active in the project and have the ability to approve pull +requests. + + * @arjunmahishi + * @ccoVeille diff --git a/README.md b/README.md index d4857d3..44d40e6 100644 --- a/README.md +++ b/README.md @@ -38,30 +38,27 @@ See it in action: package yours import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" + + "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { + // assert equality + assert.Equal(t, 123, 123, "they should be equal") - // assert equality - assert.Equal(t, 123, 123, "they should be equal") + // assert inequality + assert.NotEqual(t, 123, 456, "they should not be equal") - // assert inequality - assert.NotEqual(t, 123, 456, "they should not be equal") - - // assert for nil (good for errors) - assert.Nil(t, object) - - // assert for not nil (good when you expect something) - if assert.NotNil(t, object) { - - // now we know that object isn't nil, we are safe to make - // further assertions without causing any errors - assert.Equal(t, "Something", object.Value) - - } + // assert for nil (good for errors) + assert.Nil(t, object) + // assert for not nil (good when you expect something) + if assert.NotNil(t, object) { + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal(t, "Something", object.Value) + } } ``` @@ -74,29 +71,29 @@ if you assert many times, use the below: package yours import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" + + "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { - assert := assert.New(t) + assert := assert.New(t) - // assert equality - assert.Equal(123, 123, "they should be equal") + // assert equality + assert.Equal(123, 123, "they should be equal") - // assert inequality - assert.NotEqual(123, 456, "they should not be equal") + // assert inequality + assert.NotEqual(123, 456, "they should not be equal") - // assert for nil (good for errors) - assert.Nil(object) + // assert for nil (good for errors) + assert.Nil(object) - // assert for not nil (good when you expect something) - if assert.NotNil(object) { - - // now we know that object isn't nil, we are safe to make - // further assertions without causing any errors - assert.Equal("Something", object.Value) - } + // assert for not nil (good when you expect something) + if assert.NotNil(object) { + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal("Something", object.Value) + } } ``` @@ -120,8 +117,9 @@ An example test function that tests a piece of code that relies on an external o package yours import ( - "testing" - "github.com/stretchr/testify/mock" + "testing" + + "github.com/stretchr/testify/mock" ) /* @@ -130,8 +128,8 @@ import ( // MyMockedObject is a mocked object that implements an interface // that describes an object that the code I am testing relies on. -type MyMockedObject struct{ - mock.Mock +type MyMockedObject struct { + mock.Mock } // DoSomething is a method on MyMockedObject that implements some interface @@ -142,10 +140,8 @@ type MyMockedObject struct{ // // NOTE: This method is not being tested here, code that uses this object is. func (m *MyMockedObject) DoSomething(number int) (bool, error) { - - args := m.Called(number) - return args.Bool(0), args.Error(1) - + args := m.Called(number) + return args.Bool(0), args.Error(1) } /* @@ -155,20 +151,17 @@ func (m *MyMockedObject) DoSomething(number int) (bool, error) { // TestSomething is an example of how to use our test object to // make assertions about some target code we are testing. func TestSomething(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) - // create an instance of our test object - testObj := new(MyMockedObject) - - // set up expectations - testObj.On("DoSomething", 123).Return(true, nil) - - // call the code we are testing - targetFuncThatDoesSomethingWithObj(testObj) - - // assert that the expectations were met - testObj.AssertExpectations(t) + // set up expectations + testObj.On("DoSomething", 123).Return(true, nil) + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + // assert that the expectations were met + testObj.AssertExpectations(t) } // TestSomethingWithPlaceholder is a second example of how to use our test object to @@ -177,45 +170,42 @@ func TestSomething(t *testing.T) { // data being passed in is normally dynamically generated and cannot be // predicted beforehand (eg. containing hashes that are time sensitive) func TestSomethingWithPlaceholder(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) - // create an instance of our test object - testObj := new(MyMockedObject) + // set up expectations with a placeholder in the argument list + testObj.On("DoSomething", mock.Anything).Return(true, nil) - // set up expectations with a placeholder in the argument list - testObj.On("DoSomething", mock.Anything).Return(true, nil) - - // call the code we are testing - targetFuncThatDoesSomethingWithObj(testObj) - - // assert that the expectations were met - testObj.AssertExpectations(t) + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + // assert that the expectations were met + testObj.AssertExpectations(t) } // TestSomethingElse2 is a third example that shows how you can use // the Unset method to cleanup handlers and then add new ones. func TestSomethingElse2(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) - // create an instance of our test object - testObj := new(MyMockedObject) + // set up expectations with a placeholder in the argument list + mockCall := testObj.On("DoSomething", mock.Anything).Return(true, nil) - // set up expectations with a placeholder in the argument list - mockCall := testObj.On("DoSomething", mock.Anything).Return(true, nil) + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) - // call the code we are testing - targetFuncThatDoesSomethingWithObj(testObj) + // assert that the expectations were met + testObj.AssertExpectations(t) - // assert that the expectations were met - testObj.AssertExpectations(t) + // remove the handler now so we can add another one that takes precedence + mockCall.Unset() - // remove the handler now so we can add another one that takes precedence - mockCall.Unset() + // return false now instead of true + testObj.On("DoSomething", mock.Anything).Return(false, nil) - // return false now instead of true - testObj.On("DoSomething", mock.Anything).Return(false, nil) - - testObj.AssertExpectations(t) + testObj.AssertExpectations(t) } ``` @@ -235,35 +225,36 @@ An example suite is shown below: ```go // Basic imports import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" ) // Define the suite, and absorb the built-in basic suite // functionality from testify - including a T() method which // returns the current testing context type ExampleTestSuite struct { - suite.Suite - VariableThatShouldStartAtFive int + suite.Suite + VariableThatShouldStartAtFive int } // Make sure that VariableThatShouldStartAtFive is set to five // before each test func (suite *ExampleTestSuite) SetupTest() { - suite.VariableThatShouldStartAtFive = 5 + suite.VariableThatShouldStartAtFive = 5 } // All methods that begin with "Test" are run as tests within a // suite. func (suite *ExampleTestSuite) TestExample() { - assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) } // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(ExampleTestSuite)) + suite.Run(t, new(ExampleTestSuite)) } ``` @@ -276,33 +267,34 @@ For more information on writing suites, check out the [API documentation for the ```go // Basic imports import ( - "testing" - "github.com/stretchr/testify/suite" + "testing" + + "github.com/stretchr/testify/suite" ) // Define the suite, and absorb the built-in basic suite // functionality from testify - including assertion methods. type ExampleTestSuite struct { - suite.Suite - VariableThatShouldStartAtFive int + suite.Suite + VariableThatShouldStartAtFive int } // Make sure that VariableThatShouldStartAtFive is set to five // before each test func (suite *ExampleTestSuite) SetupTest() { - suite.VariableThatShouldStartAtFive = 5 + suite.VariableThatShouldStartAtFive = 5 } // All methods that begin with "Test" are run as tests within a // suite. func (suite *ExampleTestSuite) TestExample() { - suite.Equal(suite.VariableThatShouldStartAtFive, 5) + suite.Equal(suite.VariableThatShouldStartAtFive, 5) } // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(ExampleTestSuite)) + suite.Run(t, new(ExampleTestSuite)) } ``` @@ -329,14 +321,13 @@ Import the `testify/assert` package into your code using this template: package yours import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" + + "github.com/stretchr/testify/assert" ) func TestSomething(t *testing.T) { - - assert.True(t, true, "True is true!") - + assert.True(t, true, "True is true!") } ``` diff --git a/assert/assertion_compare.go b/assert/assertion_compare.go index 7e19eba..ffb24e8 100644 --- a/assert/assertion_compare.go +++ b/assert/assertion_compare.go @@ -390,7 +390,8 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not greater than \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater}, failMessage, msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -403,7 +404,8 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []compareResult{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not greater than or equal to \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater, compareEqual}, failMessage, msgAndArgs...) } // Less asserts that the first element is less than the second @@ -415,7 +417,8 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not less than \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareLess}, failMessage, msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -428,7 +431,8 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter if h, ok := t.(tHelper); ok { h.Helper() } - return compareTwoValues(t, e1, e2, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not less than or equal to \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareLess, compareEqual}, failMessage, msgAndArgs...) } // Positive asserts that the specified element is positive @@ -440,7 +444,8 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { h.Helper() } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []compareResult{compareGreater}, "\"%v\" is not positive", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not positive", e) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareGreater}, failMessage, msgAndArgs...) } // Negative asserts that the specified element is negative @@ -452,7 +457,8 @@ func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { h.Helper() } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []compareResult{compareLess}, "\"%v\" is not negative", msgAndArgs...) + failMessage := fmt.Sprintf("\"%v\" is not negative", e) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareLess}, failMessage, msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { @@ -468,11 +474,11 @@ func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedCompare compareResult, isComparable := compare(e1, e2, e1Kind) if !isComparable { - return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) + return Fail(t, fmt.Sprintf(`Can not compare type "%T"`, e1), msgAndArgs...) } if !containsValue(allowedComparesResults, compareResult) { - return Fail(t, fmt.Sprintf(failMessage, e1, e2), msgAndArgs...) + return Fail(t, failMessage, msgAndArgs...) } return true diff --git a/assert/assertion_order.go b/assert/assertion_order.go index 1d2f718..2fdf80f 100644 --- a/assert/assertion_order.go +++ b/assert/assertion_order.go @@ -33,7 +33,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareR compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind) if !isComparable { - return Fail(t, fmt.Sprintf("Can not compare type \"%s\" and \"%s\"", reflect.TypeOf(value), reflect.TypeOf(prevValue)), msgAndArgs...) + return Fail(t, fmt.Sprintf(`Can not compare type "%T" and "%T"`, value, prevValue), msgAndArgs...) } if !containsValue(allowedComparesResults, compareResult) { diff --git a/assert/assertions.go b/assert/assertions.go index 295144d..a9318cf 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -24,6 +24,8 @@ import ( "github.com/stretchr/testify/assert/yaml" ) +const stackFrameBufferSize = 10 + //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl" // TestingT is an interface wrapper around *testing.T @@ -212,57 +214,73 @@ the problem actually occurred in calling code.*/ func CallerInfo() []string { var pc uintptr - var ok bool var file string var line int var name string callers := []string{} - for i := 0; ; i++ { - pc, file, line, ok = runtime.Caller(i) - if !ok { - // The breaks below failed to terminate the loop, and we ran off the - // end of the call stack. + pcs := make([]uintptr, stackFrameBufferSize) + offset := 1 + + for { + n := runtime.Callers(offset, pcs) + + if n == 0 { break } - // This is a huge edge case, but it will panic if this is the case, see #180 - if file == "" { - break - } + frames := runtime.CallersFrames(pcs[:n]) - f := runtime.FuncForPC(pc) - if f == nil { - break - } - name = f.Name() + for { + frame, more := frames.Next() + pc = frame.PC + file = frame.File + line = frame.Line - // testing.tRunner is the standard library function that calls - // tests. Subtests are called directly by tRunner, without going through - // the Test/Benchmark/Example function that contains the t.Run calls, so - // with subtests we should break when we hit tRunner, without adding it - // to the list of callers. - if name == "testing.tRunner" { - break - } + // This is a huge edge case, but it will panic if this is the case, see #180 + if file == "" { + break + } - parts := strings.Split(file, "/") - if len(parts) > 1 { - filename := parts[len(parts)-1] - dir := parts[len(parts)-2] - if (dir != "assert" && dir != "mock" && dir != "require") || filename == "mock_test.go" { - callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + f := runtime.FuncForPC(pc) + if f == nil { + break + } + name = f.Name() + + // testing.tRunner is the standard library function that calls + // tests. Subtests are called directly by tRunner, without going through + // the Test/Benchmark/Example function that contains the t.Run calls, so + // with subtests we should break when we hit tRunner, without adding it + // to the list of callers. + if name == "testing.tRunner" { + break + } + + parts := strings.Split(file, "/") + if len(parts) > 1 { + filename := parts[len(parts)-1] + dir := parts[len(parts)-2] + if (dir != "assert" && dir != "mock" && dir != "require") || filename == "mock_test.go" { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } + } + + // Drop the package + segments := strings.Split(name, ".") + name = segments[len(segments)-1] + if isTest(name, "Test") || + isTest(name, "Benchmark") || + isTest(name, "Example") { + break + } + if !more { + break } } + // We know we already have less than a buffer's worth of frames + offset += stackFrameBufferSize - // Drop the package - segments := strings.Split(name, ".") - name = segments[len(segments)-1] - if isTest(name, "Test") || - isTest(name, "Benchmark") || - isTest(name, "Example") { - break - } } return callers @@ -444,7 +462,7 @@ func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs } if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { - return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + return Fail(t, fmt.Sprintf("Object expected to be of type %T, but was %T", expectedType, object), msgAndArgs...) } return true @@ -1069,7 +1087,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) element := subsetList.Index(i).Interface() ok, found := containsElement(list, element) if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...) } if !found { return true @@ -2100,7 +2118,7 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { expectedText = target.Error() } - chain := buildErrorChainString(err) + chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ "expected: %q\n"+ @@ -2123,7 +2141,7 @@ func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { expectedText = target.Error() } - chain := buildErrorChainString(err) + chain := buildErrorChainString(err, false) return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ "found: %q\n"+ @@ -2141,11 +2159,11 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{ return true } - chain := buildErrorChainString(err) + chain := buildErrorChainString(err, true) return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ - "expected: %q\n"+ - "in chain: %s", target, chain, + "expected: %s\n"+ + "in chain: %s", reflect.ValueOf(target).Elem().Type(), chain, ), msgAndArgs...) } @@ -2159,24 +2177,46 @@ func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interfa return true } - chain := buildErrorChainString(err) + chain := buildErrorChainString(err, true) return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ - "found: %q\n"+ - "in chain: %s", target, chain, + "found: %s\n"+ + "in chain: %s", reflect.ValueOf(target).Elem().Type(), chain, ), msgAndArgs...) } -func buildErrorChainString(err error) string { +func unwrapAll(err error) (errs []error) { + errs = append(errs, err) + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return + } + errs = append(errs, unwrapAll(err)...) + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + errs = append(errs, unwrapAll(err)...) + } + } + return +} + +func buildErrorChainString(err error, withType bool) string { if err == nil { return "" } - e := errors.Unwrap(err) - chain := fmt.Sprintf("%q", err.Error()) - for e != nil { - chain += fmt.Sprintf("\n\t%q", e.Error()) - e = errors.Unwrap(e) + var chain string + errs := unwrapAll(err) + for i := range errs { + if i != 0 { + chain += "\n\t" + } + chain += fmt.Sprintf("%q", errs[i].Error()) + if withType { + chain += fmt.Sprintf(" (%T)", errs[i]) + } } return chain } diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 9f859fa..9d27d1b 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -2118,7 +2118,7 @@ func TestRegexp(t *testing.T) { } for _, tc := range cases { - False(t, Regexp(mockT, tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str) + False(t, Regexp(mockT, tc.rx, tc.str), "Expected %q to not match %q", tc.rx, tc.str) False(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str)) False(t, Regexp(mockT, regexp.MustCompile(tc.rx), []byte(tc.str))) True(t, NotRegexp(mockT, tc.rx, tc.str)) @@ -2151,11 +2151,11 @@ func TestZero(t *testing.T) { mockT := new(testing.T) for _, test := range zeros { - True(t, Zero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + True(t, Zero(mockT, test, "%#v is not the %T zero value", test, test)) } for _, test := range nonZeros { - False(t, Zero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + False(t, Zero(mockT, test, "%#v is not the %T zero value", test, test)) } } @@ -2163,11 +2163,11 @@ func TestNotZero(t *testing.T) { mockT := new(testing.T) for _, test := range zeros { - False(t, NotZero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + False(t, NotZero(mockT, test, "%#v is not the %T zero value", test, test)) } for _, test := range nonZeros { - True(t, NotZero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + True(t, NotZero(mockT, test, "%#v is not the %T zero value", test, test)) } } @@ -3175,11 +3175,13 @@ func parseLabeledOutput(output string) []labeledContent { } type captureTestingT struct { - msg string + failed bool + msg string } func (ctt *captureTestingT) Errorf(format string, args ...interface{}) { ctt.msg = fmt.Sprintf(format, args...) + ctt.failed = true } func (ctt *captureTestingT) checkResultAndErrMsg(t *testing.T, expectedRes, res bool, expectedErrMsg string) { @@ -3188,6 +3190,10 @@ func (ctt *captureTestingT) checkResultAndErrMsg(t *testing.T, expectedRes, res t.Errorf("Should return %t", expectedRes) return } + if res == ctt.failed { + t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !ctt.failed) + return + } contents := parseLabeledOutput(ctt.msg) if res == true { if contents != nil { @@ -3348,50 +3354,82 @@ func TestNotErrorIs(t *testing.T) { func TestErrorAs(t *testing.T) { tests := []struct { - err error - result bool + err error + result bool + resultErrMsg string }{ - {fmt.Errorf("wrap: %w", &customError{}), true}, - {io.EOF, false}, - {nil, false}, + { + err: fmt.Errorf("wrap: %w", &customError{}), + result: true, + }, + { + err: io.EOF, + result: false, + resultErrMsg: "" + + "Should be in error chain:\n" + + "expected: *assert.customError\n" + + "in chain: \"EOF\" (*errors.errorString)\n", + }, + { + err: nil, + result: false, + resultErrMsg: "" + + "Should be in error chain:\n" + + "expected: *assert.customError\n" + + "in chain: \n", + }, + { + err: fmt.Errorf("abc: %w", errors.New("def")), + result: false, + resultErrMsg: "" + + "Should be in error chain:\n" + + "expected: *assert.customError\n" + + "in chain: \"abc: def\" (*fmt.wrapError)\n" + + "\t\"def\" (*errors.errorString)\n", + }, } for _, tt := range tests { tt := tt var target *customError t.Run(fmt.Sprintf("ErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) { - mockT := new(testing.T) + mockT := new(captureTestingT) res := ErrorAs(mockT, tt.err, &target) - if res != tt.result { - t.Errorf("ErrorAs(%#v,%#v) should return %t", tt.err, target, tt.result) - } - if res == mockT.Failed() { - t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !mockT.Failed()) - } + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) }) } } func TestNotErrorAs(t *testing.T) { tests := []struct { - err error - result bool + err error + result bool + resultErrMsg string }{ - {fmt.Errorf("wrap: %w", &customError{}), false}, - {io.EOF, true}, - {nil, true}, + { + err: fmt.Errorf("wrap: %w", &customError{}), + result: false, + resultErrMsg: "" + + "Target error should not be in err chain:\n" + + "found: *assert.customError\n" + + "in chain: \"wrap: fail\" (*fmt.wrapError)\n" + + "\t\"fail\" (*assert.customError)\n", + }, + { + err: io.EOF, + result: true, + }, + { + err: nil, + result: true, + }, } for _, tt := range tests { tt := tt var target *customError t.Run(fmt.Sprintf("NotErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) { - mockT := new(testing.T) + mockT := new(captureTestingT) res := NotErrorAs(mockT, tt.err, &target) - if res != tt.result { - t.Errorf("NotErrorAs(%#v,%#v) should not return %t", tt.err, target, tt.result) - } - if res == mockT.Failed() { - t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !mockT.Failed()) - } + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) }) } } diff --git a/assert/forward_assertions_test.go b/assert/forward_assertions_test.go index 496d3c0..5752d44 100644 --- a/assert/forward_assertions_test.go +++ b/assert/forward_assertions_test.go @@ -546,7 +546,7 @@ func TestRegexpWrapper(t *testing.T) { } for _, tc := range cases { - False(t, assert.Regexp(tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str) + False(t, assert.Regexp(tc.rx, tc.str), "Expected %q to not match %q", tc.rx, tc.str) False(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str)) True(t, assert.NotRegexp(tc.rx, tc.str)) True(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str)) diff --git a/assert/http_assertions.go b/assert/http_assertions.go index 861ed4b..5a6bb75 100644 --- a/assert/http_assertions.go +++ b/assert/http_assertions.go @@ -138,7 +138,7 @@ func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, contains := strings.Contains(body, fmt.Sprint(str)) if !contains { - Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body), msgAndArgs...) + Fail(t, fmt.Sprintf("Expected response body for %q to contain %q but found %q", url+"?"+values.Encode(), str, body), msgAndArgs...) } return contains @@ -158,7 +158,7 @@ func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url strin contains := strings.Contains(body, fmt.Sprint(str)) if contains { - Fail(t, fmt.Sprintf("Expected response body for \"%s\" to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body), msgAndArgs...) + Fail(t, fmt.Sprintf("Expected response body for %q to NOT contain %q but found %q", url+"?"+values.Encode(), str, body), msgAndArgs...) } return !contains diff --git a/mock/mock.go b/mock/mock.go index eb5682d..c81a0bd 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -208,9 +208,16 @@ func (c *Call) On(methodName string, arguments ...interface{}) *Call { return c.Parent.On(methodName, arguments...) } -// Unset removes a mock handler from being called. +// Unset removes all mock handlers that satisfy the call instance arguments from being +// called. Only supported on call instances with static input arguments. // -// test.On("func", mock.Anything).Unset() +// For example, the only handler remaining after the following would be "MyMethod(2, 2)": +// +// Mock. +// On("MyMethod", 2, 2).Return(0). +// On("MyMethod", 3, 3).Return(0). +// On("MyMethod", Anything, Anything).Return(0) +// Mock.On("MyMethod", 3, 3).Unset() func (c *Call) Unset() *Call { var unlockOnce sync.Once @@ -331,7 +338,10 @@ func (m *Mock) TestData() objx.Map { Setting expectations */ -// Test sets the test struct variable of the mock object +// Test sets the [TestingT] on which errors will be reported, otherwise errors +// will cause a panic. +// Test should not be called on an object that is going to be used in a +// goroutine other than the one running the test function. func (m *Mock) Test(t TestingT) { m.mutex.Lock() defer m.mutex.Unlock() @@ -494,7 +504,7 @@ func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Argumen // expected call found, but it has already been called with repeatable times if call != nil { m.mutex.Unlock() - m.fail("\nassert: mock: The method has been called over %d times.\n\tEither do one more Mock.On(\"%s\").Return(...), or remove extra call.\n\tThis call was unexpected:\n\t\t%s\n\tat: %s", call.totalCalls, methodName, callString(methodName, arguments, true), assert.CallerInfo()) + m.fail("\nassert: mock: The method has been called over %d times.\n\tEither do one more Mock.On(%#v).Return(...), or remove extra call.\n\tThis call was unexpected:\n\t\t%s\n\tat: %s", call.totalCalls, methodName, callString(methodName, arguments, true), assert.CallerInfo()) } // we have to fail here - because we don't know what to do // as the return arguments. This is because: @@ -514,7 +524,7 @@ func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Argumen assert.CallerInfo(), ) } else { - m.fail("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(\"%s\").Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", methodName, methodName, callString(methodName, arguments, true), assert.CallerInfo()) + m.fail("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(%#v).Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", methodName, methodName, callString(methodName, arguments, true), assert.CallerInfo()) } } diff --git a/require/doc.go b/require/doc.go index 9684347..c8e3f94 100644 --- a/require/doc.go +++ b/require/doc.go @@ -23,6 +23,8 @@ // // The `require` package have same global functions as in the `assert` package, // but instead of returning a boolean result they call `t.FailNow()`. +// A consequence of this is that it must be called from the goroutine running +// the test function, not from other goroutines created during the test. // // Every assertion function also takes an optional string message as the final argument, // allowing custom error messages to be appended to the message the assertion method outputs.