Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking external struct dependencies in golang

I am having a hard time figuring out an idiomatic way of writing testable code in golang. I understand the importance of interfaces and their use in testing, but I haven't figured out how to mock/test external struct dependencies.

As an example, I have written the following which simulates a wrapper for creating a pull request on GitHub.

type GitHubService interface {

}

type gitHubService struct {
    CreatePullRequest(...) (PullRequest,error)
}

func (s gitHubService) CreatePullRequest(...) (PullRequest,error) {
  tp := github.BasicAuthTransport{
      Username: strings.TrimSpace(/*.....*/),
      Password: strings.TrimSpace(/*.....*/),
    }

    client := github.NewClient(tp.Client())
  pr,err := client.Repositories.CreatePullRequest(...)

  ...
}

func TestPullRequest(t *testing.T) {
  service := gitHubService{}
  pr,err := service.CreatePullRequest(...)
  ...
}

If I was writing a unit test for GitHubService.CreatePullRequest(...) I would want to mock the call to client.Repositories.CreatePullRequest(...) and probably even github.NewClient(...) to return mock implementations that I can control.

With tools such as gomock it seems that you are out of luck with structs and package functions.

What is the idiomatic way to handle this? I am very familiary with Inversion of Control and the different patterns such as Dependency Injection and Service Locator, but I have heard countless times that this is not idiomatic.

like image 834
TheJediCowboy Avatar asked Feb 06 '23 00:02

TheJediCowboy


2 Answers

One important design feature of Go is decoupling (Watch this great talk from Bill Kennedy about that topic). Inside your method there are some dependencies, which could be decoupled. This coupled method makes it not really testable.

Thing you should refactor:

  • tp := github.BasicAuthTransport: you should not initialize the authorization inside of your method. It should move into your gitHubService as a parameter. Inside your method call you can access ist via s.tp. You could also make it an input parameter of the method.
  • github.NewClient() and client.Repositories.CreatePullRequest(...) just read about the golang best practices from Peter Bourgon Make dependencies explicit!. The alternative is to create an interface, which contains all the called functions. This interface should be an input to your method.

After your code is decoupled you can mock everything very easy. If you use interfaces as an input you can just create a mock struct, which implements the interface. If you make the dependencies explicit you can overwrite them. In the last case the code for storing the values of the calls is not so clean, but it also works. The idiomatic go way is to use interfaces.

like image 79
apxp Avatar answered Feb 20 '23 10:02

apxp


I had a similar problem and looks like, the only option you have is to have another layer in between which would call your "unmockable" clients.

For e.g. for mocking the govmi clients (vmware clients sdk for golang), I had to have a "myCustomClient" having interfaces and structs to make calls to govmi.Client.AnyMethod..

I could then generate mocks for "myCustomClient".

mockgen -source myCustomClient.go -package myPackage -destination myCustomClientMock.go

You can install it by: got get github.com/golang/mock

like image 30
Abhishek Srivastava Avatar answered Feb 20 '23 11:02

Abhishek Srivastava