maraino / go-mock

A mocking framework for the Go Programming Language.
MIT License
35 stars 12 forks source link

Support variable number of arguments #6

Closed clawconduce closed 8 years ago

clawconduce commented 8 years ago

I have some use cases for database queries where I want any database query to return an error. The number of arguments changes, so the Any* variables do not work.

My thought was to add a AnyZeroOrMoreArgs (the name should be improved) that would break out of this loop: https://github.com/maraino/go-mock/blob/master/mock.go#L287 along with adjust other required checks. If AnyZeroOrMoreArgs was the only arg passed in to a mock, then it would match any call to a function regardless of how many args are passed in. The AnyZeroOrMoreArgs could also be passed in after specifying some number of args. So for example, if your code takes in a formatting string & a variable number of args after that, you could pass in AnythingOfType("string"), AnymoreArgs.

willfaught commented 8 years ago

See willfaught/gockle#1 for the background.

I was toying around with how it would look to have something like this. Normally, for a vararg func, you can only specify mock.Any or an entire concrete slice value. Something like:

m.When("F", mock.Any)
m.When("F", []interface{}{"a", "test"})

but when you want to assert concrete values for some varargs and mock.Any for others, you're stuck using a verbose mock.AnyIf like this:

// Match 2 varargs where the first is anything and the second is "test"
m.When("F", mock.AnyIf(func(v interface{}) bool {
    var s, ok = v.([]interface{})
    if !ok || len(s) != 2 {
        return false
    }
    return s[1] == "test"
}))

Wouldn't it be nice if you could do something like this:

m.When("F", mock.Varargs{mock.Any, "test"})

But that got me thinking that this is really just a special case of the more general problem of wanting to nest mock.Any inside any slice, like this:

m.When("G", mock.Slice{mock.Any, "test"}, 123, mock.Slice{"a", mock.Any}) // Last mock.Slice is the varargs, or it could be a normal slice param

So basically this could all be accomplished with mock.AnyIf shorthands for nested mock variables in slices. I imagine the same for maps or channels might also be useful.

willfaught commented 8 years ago

And to address @clawconduce's point about only wanting to specify a prefix of the slice, you could put some mock.Rest value at the end of the mock.Slice values as he detailed.

willfaught commented 8 years ago

Something like

type Slice []interface{}
type Map map[interface{}]interface{}
type Chan []interface{}

with nested matching logic in Mock.find.

maraino commented 8 years ago

As @clawconduce this is an issue we don't have a good solution yet, as @willfaught comments, we use just one argument and write this as a mock.Any or as a []interface{}{...}

But if you want to return variables as arguments, it might be a little more difficult to do, one thing that has worked for me is the use of the Call method:

// Scan is the mock implementation
func (m *Query) Scan(dest ...interface{}) error {
    ret := m.Called(dest)
    ret0, _ := ret.Get(0).(error)
    return ret0
}

// NewQueryScan returns a *Query with Scan mocked returning the args that we want.
func NewQueryScan(args ...interface{}) *Query {
    query := &Query{}
    query.When("Scan", mock.Any).Call(func(out ...interface{}) error {
        for i := range args {
            reflect.ValueOf(out[i]).Elem().Set(reflect.ValueOf(args[i]))
        }
        return nil
    })
    return query
}

As in Go you cannot have two methods with the same name but different number of arguments, I will probably remove the restriction on the number on arguments, so you don't need the mock.Any there.

willfaught commented 8 years ago

PR maraino/go-mock/pull/7 solves this.

clawconduce commented 8 years ago

Cool! Thanks everyone!