elliotchance / redismock

🕋 Mocking Redis in unit tests in Go.
MIT License
147 stars 24 forks source link

Stricter call matching possible? #36

Open tommyalatalo opened 3 years ago

tommyalatalo commented 3 years ago

I'm having problems with redismock catching other calls for the same operation, which shouldn't be mocked.

For instance, I have a function where I call Scan two times, and I only want to mock the second call to simulate an error being returned. This seems to not work since redismock thinks it should mock the first Scan call as well, even though the key has not been set up to be mocked.

An example in code:

    keys, _, err := client.Scan(ctx, 0, "first:*", 0).Result()
    if err != nil {
        return nil, err
    }

    keys, _, err := client.Scan(ctx, 0, "second:*", 0).Result()
    if err != nil {
        return nil, err
    }

Now if I mock the second call in my test suite like this:

tr.On("Scan", ctx, uint64(0), "second:*", int64(0)).Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))

redismock will return this error:

Scan(*context.emptyCtx,uint64,string,int64)
        0: (*context.emptyCtx)(0xc00011e010)
        1: 0x0
        2: "first:*"
        3: 0

The closest call I have is: 

Scan(*context.emptyCtx,uint64,string,int64)
        0: (*context.emptyCtx)(0xc00011e010)
        1: 0x0
        2: "second:*"
        3: 0

I'm using NewNiceMock to create my test client. Is there a way to set the mocking to be stricter, so that any calls that are not defined with the .On function will be completely ignored and not fuzzy-matched or whatever is happening here now?

The behavior that I'm getting now is requiring me to mock both calls, which means that the first call can't use miniredis to get real data, which is of course not desirable.

elliotchance commented 3 years ago

I can't test it right now, but I'm pretty sure testify lets you do this by providing different expecations:

tr.On("Scan", ctx, uint64(0), "first:*", int64(0)).
  Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))

tr.On("Scan", ctx, uint64(0), "second:*", int64(0)).
  Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))

Alternatively, you could use DoAndReturn() if you wanted to handle the arguments dynamically.

tommyalatalo commented 3 years ago

I can't test it right now, but I'm pretty sure testify lets you do this by providing different expecations:

tr.On("Scan", ctx, uint64(0), "first:*", int64(0)).
  Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))

tr.On("Scan", ctx, uint64(0), "second:*", int64(0)).
  Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))

Alternatively, you could use DoAndReturn() if you wanted to handle the arguments dynamically.

I don't see any difference in the example mocks you defined here except for the path in redis? Not sure what you wanted to point out with those two mocks, that's kind of another example of my problem, if you want to mock second but not first it doesn't seem to work to just omit the mock for first.

I would be really interested in how you would use DoAndReturn() to handle this case. The best, and preferred solution, I think would still be to have redismock strictly only mock the calls you define with .On(), if it's not already doing that and I'm misunderstanding the way it works.

elliotchance commented 3 years ago

I see, I didn't understand you at first. I haven't tested these, but this should do what you want:

tr.On("Scan", ctx, uint64(0), mock.Anything, int64(0)).
  Times(2).
  DoAndReturn(func (ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd {
      if match == "second:*" {
        return redis.NewScanCmdResult([]string{}, uint64(0), test.err)
      }

      return client.Scan(ctx, cursor, match, count)
  })

Or, if you prefer to have separate expections:

tr.On("Scan", ctx, uint64(0), "first:*", int64(0)).
  DoAndReturn(client.Scan)

tr.On("Scan", ctx, uint64(0), "second:*", int64(0)).
  Return(redis.NewScanCmdResult([]string{}, uint64(0), test.err))
tommyalatalo commented 3 years ago

I tried to use this one of your examples:

tr.On("Scan", ctx, uint64(0), "first:*", int64(0)).DoAndReturn(client.Scan)

This can't be done though, because `type *mock.Call has no field or method DoAndReturn?

elliotchance commented 3 years ago

Sorry, I got my mocking frameworks mixed up. Unfortunatly, testify mock package doesn't have a way to dynamically return, but a quick search brought this up: https://github.com/stretchr/testify/issues/350 which will solve your problem.