rafaeljusto/redigomock

Feature: fail when command stub isn't called

Closed this issue · 7 comments

rylnd commented

It would be great if, in addition to setting command stubs, there was an option to fail if said stub was not called during the test (Similar to RSpec's allow vs expect syntax).

Apologies if this is already part of the library, I was unable to find it if so.

That's a great idea! We do not have anything like that in the library. The Cmd type could have the following methods:

func (c *Cmd) Expect(response interface{}) *Cmd {...}
func (c *Cmd) ExpectMap(response map[string]string) *Cmd {...}
func (c *Cmd) ExpectError(err error) *Cmd {...}

func (c *Cmd) Allow(response interface{}) *Cmd {...}
func (c *Cmd) AllowMap(response map[string]string) *Cmd {...}
func (c *Cmd) AllowError(err error) *Cmd {...}

Where Expect would be used when you want the command to be called and Allow to when you don't care. This approach wouldn't break the current API. Now to check the stubs that weren't called I was thinking in a new function on Conn Type.

func (c Conn) UncalledCmds() []Cmd {...}

Not sure about this function name yet. We could also have some kind of statistics about how many times a command was called.

What do you think?

rylnd commented

Yeah, I like the basic idea. It might be more flexible to expose a map[string]int of function names -> call counts; that way you could assert not only whether a function was called, but also how many times (if you so choose).

If we expose this map in the Conn type, do we still need to have the new Allow functions? One problem that I see with map[string]int is that using only the function name (GET, HGET, SET, HSET, ...) as the map key can create conflicts. To make it unique we need to add the function name + arguments, and as we can't create a comparable type containing a slice (arguments), we couldn't use it as a map key.

rylnd commented

Right, the Allow wouldn't be needed in your proposed approach. I was imagining a situation in which Allow would accept the *testing.T and fail automatically, but I see that you've avoided coupling to that package thus far.

I see your point about the maps, although I imagine there's some way to hash the function/arguments into a valid key. I'm pretty new to Go, so I defer to you for implementation.

So I think we have the following solution:

  • Add a attribute to connection to store command's statistics
type Conn struct {
    ...
    CommandsStats map[CmdHash]int
}
  • Add a hash function to the Cmd type to use it as the key map
type CmdHash string

func (c Cmd) Hash() CmdHash {
    output := c.Name
    for _, arg := range c.Args {
        output += fmt.Sprintf("%v", arg)
    }
    return output
}

An example of use would be:

func main() {
    ...

    conn := redigomock.NewConn()
    cmd := conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{
        "name": "Mr. Johson",
        "age":  "42",
    })

    person, err := RetrievePerson(conn, "1")
    if err != nil {
        fmt.Println(err)
        return
    }

    if conn.CommandsStats[cmd.Hash()] == 0 {
        fmt.Println("was not called!")
        return
    }

    ...
}

From this point we could also add some helpful methods to the Conn type for a cleaner check. For example func (c Conn) Stats(Cmd) int. I will think about it this weekend.

By the way, we could only export the method Stats from the Conn type:

type Conn struct {
    ...
    stats map[cmdHash]int
}

func (c Conn) Stats(cmd Cmd) int {
  return c.stats[cmd.hash()]
}
type cmdHash string

func (c Cmd) hash() cmdHash {
    output := c.Name
    for _, arg := range c.Args {
        output += fmt.Sprintf("%v", arg)
    }
    return cmdHash(output)
}
func main() {
    ...

    conn := redigomock.NewConn()
    cmd := conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{
        "name": "Mr. Johson",
        "age":  "42",
    })

    person, err := RetrievePerson(conn, "1")
    if err != nil {
        fmt.Println(err)
        return
    }

    if conn.Stats(cmd) == 0 {
        fmt.Println("was not called!")
        return
    }

    ...
}

Not sure if Stats is a good name for retrieving the number of times that the command was called.