How to mock a third-party struct with many methods and perform unit tests on endpoints that depend on the third-party struct?

Issue

I’m working with getstream’s Go library in gin gonic and realized that my endpoints will be heavily dependent on stream_chat.Client.

For instance, in the following endpoint (/v1/chat/test-token), a stream_chat.Client must be created so testing this endpoint in unit test would mean creating and maintaining an interface that documents all the methods I use from stream_chat.Client so that I can perform dependency injection with a MockClient that satisfies the same interface and then I can mock the methods chatClient.UpsertUser and chatClient.CreateToken when I write my unit test.

func main() {
    config.Load()

    server := gin.New()

    chatClient, err := stream_chat.NewClient(config.StreamApiKey, config.StreamApiSecret)
    if err != nil {
        log.Err(err)
        os.Exit(2)
    }

    v1 := server.Group("/v1")
    {
        v1.GET("/chat/test-token/", func(c *gin.Context) {
            _, err := chatClient.UpsertUser(&stream.User{
                ID:   "test-user",
                Role: "admin",
            })

            if err != nil {
                c.JSON(http.StatusInternalServerError, gin.H{})
            }

            token, _ := chatClient.CreateToken("test-user", time.Time{})
            c.JSON(http.StatusOK, gin.H{
                "token": token,
            })
        })
    }
    
    server.Run(fmt.Sprintf(":%s", config.Port))
}

It seems to me to be quite laborious to document each method that I’d use from stream_chat.Client in order to keep a good test coverage on the endpoints, so I wonder what one should do in this case?

  1. Is maintaining an interface for stream_chat.Client the correct way to go?
  2. Less relevant: Is there a way to properly decouple the gin.HandlerFunc, i.e. func(c *gin.Context) from the creation of stream_chat.Client?
  3. Even less relevant: Is it better to create a singleton stream_chat.Client or should I create a new client for each endpoint that requires a client?

Solution

Is maintaining an interface for stream_chat.Client the correct way to go?

If you have a non-interface dependency and you wish to unit test handlers with that, then yes. You need to wrap stream_chat.Client in an interface.

If the third-party struct has a lot of methods, you could split the interface in logical units and inject in each handler only those that are actually needed. The underlying stream_chat.Client implements all of them, but the individual mocks can be kept small and easier to reason about. Personally, I don’t think it’s worth the overhead. There’s plenty of open-source mock generators, above all mock and mockgen, and also tools that generate interfaces from structs.

Is there a way to properly decouple the gin.HandlerFunc, i.e. func(c *gin.Context) from the creation of stream_chat.Client?

You have several options, which you can find here: How to pass arguments to router handlers in Golang using Gin web framework?

In short, the options I prefer are due to better unit-testability are:

  1. make the handlers methods of a struct and your dependencies fields of this struct.
  2. use a provider pattern and set the provider into the Gin context in a middleware

Answered By – blackgreen

Answer Checked By – Mildred Charles (GoLangFix Admin)

Leave a Reply

Your email address will not be published.