0

TL;DR: mocked method accepts closure. I wonder how to create custom matcher (https://godoc.org/github.com/golang/mock/gomock#Matcher): closure itself in turn is working with private structure - meaning I can't even call the closure in my test to check it against expectations.


I'm working on a small app using Slack API with help of nlopes/slack (https://github.com/nlopes/slack).

For testing, I'm mocking nlopes/slack with gomock. For that I've created interface

type slackAPI interface {
    OpenConversation(*slack.OpenConversationParameters) (*slack.Channel, bool, bool, error)
    PostMessage(channelID string, options ...slack.MsgOption) (string, string, error)
    GetUserByEmail(email string) (*slack.User, error)
}

I have no problem testing OpenConversation or GetUserByEmail, e.g.

slackAPIClient.
    EXPECT().
    GetUserByEmail("some@email.com").
    Return(slackUserJohndoe, nil).
    Times(1)

Things get more complicated when it comes to PostMessage. In main code the call looks like

_, _, err := slackAPIClient.PostMessage(channel.ID, slack.MsgOptionText(message, false))

And slack.MsgOptionText (from nlopes/slack) is actually returning closure:

func MsgOptionText(text string, escape bool) MsgOption {
    return func(config *sendConfig) error {
        if escape {
            text = slackutilsx.EscapeMessage(text)
        }
        config.values.Add("text", text)
        return nil
    }
}

Since method is accepting closure, I need to create custom gomock matcher (https://godoc.org/github.com/golang/mock/gomock#Matcher). Custom matcher itself is not a problem, it would look something like

type higherOrderFunctionEqMatcher struct {
    x interface{}
}

func (e hofEqMatcher) Matches(x interface{}) bool {
    //return m.x == x
    return true
}

func (e hofEqMatcher) String(x interface{}) string {
    return fmt.Sprintf("is equal %v", e.x)
}

However, since MsgOptionText uses nlopes/slack private structure sendConfig, I wonder how can I even work with that in scope of my test to check equality to expectations.

How should I tackle such problem?

Nikita
  • 31
  • 5
  • 1
    Not sure I completely understand what you're trying to do, but just so you know, in Go, it is not possible to compare functions, i.e. you cannot check if a passed in closure matches the expected closure, it's just not possible, not in Go. – mkopriva Feb 29 '20 at 15:15
  • I'm trying to create unit tests for my code. For that I'm mocking Slack API (it's nlopes/slack implementation) - so that unit test would not any real calls. Then I'm setting expectations for mocked functions. It's not a problem when mocked function accepts regular variables, structs etc. You just compare them. It seem to be a problem with mocked function accepts closure (it's by design of nlopes/slack module, not my choise). Because you can't compare closures in Go. So, I'm trying to figure out how to test such function :) – Nikita Feb 29 '20 at 15:17
  • 1
    I think that in this case the best you can do is to check that the passed in closure is not `nil`, that's about it. – mkopriva Feb 29 '20 at 15:29
  • Just stop mocking and write plain fakes. That is much saner. – Volker Feb 29 '20 at 15:53
  • My app is requesting JIRA to get some info and then pokes corresponding people in Slack. For testing of JIRA part - plain fakes work perfectly. But for testing of Slack part - in order to test my app, I do need to use mocks to verify behavior (basically, which Slack calls did my app make). Right? – Nikita Feb 29 '20 at 15:59
  • Thank you, @mkopriva and @Volker! Seem that I've found solution, updated main text. – Nikita Mar 01 '20 at 07:54

1 Answers1

1

Bearing in mind that

  1. in Golang you can't compare functions
  2. in this precise case I can't do indirect test by calling closure itself (since it's using private 3rd party lib's structure as an argument)

the solution I've found is to mock slack.MsgOptionText(message, false), which in turn returns closure for PostMessage(channelID string, options ...slack.MsgOption):

type slackMsgCreator interface {
    MsgOptionText(string, bool) slack.MsgOption
}

type slackMsgCreatorInst struct{}

func (s slackMsgCreatorInst) MsgOptionText(text string, escape bool) slack.MsgOption {
    return slack.MsgOptionText(text, escape)
}

...

slackMsgCreator.
    EXPECT().
    MsgOptionText("Dear John Doe, message goes here", false).
    Return(slack.MsgOptionText("Dear John Doe, message goes here", false)).
    Times(1)

And, as for PostMessage - as was advised in comments, the only thing that I could check is that closure is not nil:

slackAPIClient.
    EXPECT().
    PostMessage("ABCDE", Not(Nil())).
    AnyTimes()
Nikita
  • 31
  • 5