What's the difference between stub and mock in Go unit testing? - unit-testing

I am using mocks in unit testing in Go. How do I understand the difference between stub and mock in the implementation code in Go?

Intention of mocks and stubs in GO is the same as different programming languages:
stub is replacement for some dependency in your code that will be used during test execution. It is typically built for one particular test and unlikely can be reused for another because it has hardcoded expectations and assumptions.
mock takes stubs to next level. It adds means for configuration, so you can set up different expectations for different tests. That makes mocks more complicated, but reusable for different tests.
Let's check how that works on example:
In our case, we have http handler that internally makes http calls to another web service. To test handler we want to isolate handler code from dependency we do not control (external web service). We can do that by either using stub or mock.
Our handler code is the same for stub and mock. We should inject http.Client dependency to be be able to isolate it in unit test:
func New(client http.Client) http.Handler {
return &handler{
client: client,
}
}
type handler struct {
client http.Client
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
// work with external web service that cannot be executed in unit test
resp, err := h.client.Get("http://example.com")
...
}
Our replacement for run-time http.Client is straight-forward in stub:
func TestHandlerStub(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// here you can put assertions for request
// generate response
w.WriteHeader(http.StatusOK)
}))
server := httptest.NewServer(mux)
r, _ := http.NewRequest(http.MethodGet, "https://some.com", nil)
w := httptest.NewRecorder()
sut := New(server.Client())
sut.ServeHTTP(w, r)
//assert handler response
}
Mock story is more complicated. I am skipping code for mock implementation but this is how its interface may look like:
type Mock interface {
AddExpectation(path string, handler http.HandlerFunc)
Build() *http.Client
}
This is code for test using Mock:
func TestHandlerMock(t *testing.T) {
mock := NewMock()
mock.AddExpectation("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// here you can put assertions for request
// generate response
w.WriteHeader(http.StatusOK)
}))
r, _ := http.NewRequest(http.MethodGet, "https://some.com", nil)
w := httptest.NewRecorder()
sut := New(mock.Build())
sut.ServeHTTP(w, r)
//assert handler response
}
For this simple sample it adds no much value. But think about more complicated cases. You can build much cleaner tests code and cover more cases with less lines.
This is how tests setup may look like if we have to call 2 services and evolved our mock a little bit:
mock.AddExpectation("/first", firstSuccesfullHandler).AddExpectation("/second", secondSuccesfullHandler)
mock.AddExpectation("/first", firstReturnErrorHandler).AddExpectation("/second", secondShouldNotBeCalled)
mock.AddExpectation("/first", firstReturnBusy).AddExpectation("/first", firstSuccesfullHandler)AddExpectation("/second", secondSuccesfullHandler)
You can imagine how many times you have to copy-paste handler logic in tests if we do not have our tiny mock helper. That copy-pasted code makes our tests fridgile.
But building your own mocks is not the only options. You can rely on existing mocking packages like DATA-DOG/go-sqlmock that mocks SQL.

Related

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

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?
Is maintaining an interface for stream_chat.Client the correct way to go?
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?
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?
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:
make the handlers methods of a struct and your dependencies fields of this struct.
use a provider pattern and set the provider into the Gin context in a middleware

mocking and spying in golang without DI

Is there a way to mock and/or spy on methods in golang without using dependency injection. For example lets say that I want to unit test the following method
import (
"github.com/spf13/viper"
"google.golang.org/grpc"
)
func startGrpcServer(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
listen, err := net.Listen("tcp", fmt.Sprintf(":%v", viper.GetString("GRPC_PORT")))
if err != nil {
log.Fatal(err)
}
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
reflection.Register(grpcServer)
s := health.NewHealthServer()
proto.RegisterHealthServer(grpcServer, s)
go func() {
<-ctx.Done()
log.Print("Stopping grpc server")
grpcServer.GracefulStop()
}()
log.Print("Starting grpc server")
grpcServer.Serve(listen)
}
I want to make sure of the following
The server is started (spying on the grpcServer.Serve method.
The environment value is ready from viper.GetString method.
As you can see the packages for these methods are global imports and I am not using DI. Also this is legacy code so I cannot go and change all the code to support DI. From my reading and initial researches done you cannot use mock/spy in golang without DI. Is this correct ? or is there some way to do this ?
I came across the following but this is fairly dated How do I mock a function from another package without using dependency injection? and from what I see in this, my options are fairly limited.
Is there a way to mock and/or spy on methods in golang without using dependency injection.
No, there isn't.

Using Two Different Mocks in Go Unit Test

I am learning to do unit testing using mocks in Go for the first time, using gomock's mockgen utility. My unit tests work fine except for one of them. The method-under-test has two dependencies: one on a database, and the other on an external service it makes rest api calls to. The mock for the database (mockRepo) works fine in that the method-under-test properly invokes the mock instead of the actual repo code. The mock for the rest client, however, continues to invoke the actual rest client and not the mock code. I can't figure out why. Can someone explain why and help fix?
Here is my unit test:
func TestService_CreateWorkspace(t *testing.T) {
ts := NewTestService(t)
defer ts.mockCtrl.Finish()
ts.mockClient.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&http.Response{StatusCode: 200}, nil)
testWs := TestWorkspaces()["max-ws"]
ts.mockRepo.EXPECT().Create(testWs).Times(1).Return(&testWs, nil)
ws, err := ts.service.CreateWorkspace(&testWs)
assert.Equal(t, testWs, ws)
assert.NoError(t, err)
}
Here is the code for NewTestService:
type TestService struct {
mockCtrl *gomock.Controller
mockClient *MockRestClient
mockRepo *MockRepository
service Service
}
func NewTestService(t *testing.T) *TestService {
mockCtrl := gomock.NewController(t)
mockRepo := NewMockRepository(mockCtrl)
mockClient := NewMockRestClient(mockCtrl)
return &TestService{
mockCtrl: mockCtrl,
mockClient: mockClient,
mockRepo: mockRepo,
service: NewService(mockRepo),
}
}
Is there an issue with assigning the same mock controller to two different mock objects? Not really sure what's going on here. Any help appreciated.
I resolved this as Adrian in the comments above suggested. I was missing a way to pass the mock client into the NewService and ended up adding a client parameter to NewService.

Unit Testing Google's Go API Client

I am currently writing a workflow in Go that uses Google's API go client. I'm relatively new to Go and am having trouble unit testing the client's services. Here is an example method that enables an API in a Google Cloud Project.
func (gcloudService *GCloudService) EnableApi(projectId string, apiId string) error {
service, err := servicemanagement.New(gcloudService.Client)
if err != nil {
return err
}
requestBody := &servicemanagement.EnableServiceRequest{
ConsumerId: consumerId(projectId),
}
_, err = service.Services.Enable(apiId, requestBody).Do()
if err != nil {
return err
}
return nil
}
GCloudService is a simple struct that holds a Client.
type GCloudService struct {
Client *http.Client
}
This is my attempt at testing this method.
var (
mux *http.ServeMux
client *http.Client
server *httptest.Server
)
func setup() {
// test server
mux = http.NewServeMux()
server = httptest.NewServer(mux)
// client configured to use test server
client = server.Client()
}
func teardown() {
server.Close()
}
func TestGCloudService_EnableApi(t *testing.T) {
setup()
defer teardown()
projectName := "test"
apiId := "api"
testGcloudService := &GCloudService{
Client: client,
}
path := fmt.Sprintf("/v1/services/{%s}:enable", apiId)
mux.HandleFunc(path,
func(w http.ResponseWriter, r *http.Request) {
// test things...
})
err := testGcloudService.EnableApi(projectName, apiId)
if err != nil {
t.Errorf("EnableApi returned error: %v", err)
}
}
However, when I run this test it still hits the real Google endpoint instead of my localhost server because EnableApi uses the servicemanagement service which is configured with the API's base URL. How do I refactor this to call my server instead of the API? I am hoping to avoid mocking the entire servicemanagement service if possible.
What I'd recommend is creating your own interface that wraps the google api client and extract the methods that you're interested in.
type MyWrapperClient interface {
SomeMethodWithCorrectReturnType()
}
type myWrapperClient struct {
*GCloudService.Client // or whatever
}
In the directory I'd then run:
mockery -name=MyWrapperClient inside the directory (after installing mockery)
and then you can access your mocked version. Then on object creation substitute your mock in for your client - as the interface and the mock have the same methods they are interchangeable. Then you can test whether methods are called with specific params - leaving the google api client code alone.
More information on the mockery library is here: https://github.com/vektra/mockery
This article solves your same problem and it's absolutely fantastic in explaining how to mock and abstract your concerns away.
https://medium.com/agrea-technogies/mocking-dependencies-in-go-bb9739fef008
Make the base url in your servicemanagement service configurable or overwritable, and if that is hidden for you, then your code is not written for test convenience, change that, and if not possible, complain to who is responsible. If that does not help, take a deep breath, and write a mock service, which is mostly not needed to be very complicated

How to mock http.Head()

I'm studying the outyet example project from https://github.com/golang/example/tree/master/outyet. The test file does not cover the case where http.Head(url) returns an error. I would like to extend the unit tests to cover the if statement where the error is logged (https://github.com/golang/example/blob/master/outyet/main.go#L100). I would like to mock http.Head(), but I'm not sure how to do this. How can this be done?
The http.Head function simply calls the Head method on the default HTTP client (exposed as http.DefaultClient). By replacing the default client within your test, you can change the behaviour of these standard library functions.
In particular, you will want a client that sets a custom transport (any object implementing the http.RoundTripper interface). Something like the following:
type testTransport struct{}
func (t testTransport) RoundTrip(request *http.Request) (*http.Response, error) {
# Check expectations on request, and return an appropriate response
}
...
savedClient := http.DefaultClient
http.DefaultClient = &http.Client{
Transport: testTransport{},
}
# perform tests that call http.Head, http.Get, etc
http.DefaultClient = savedClient
You could also use this technique to mock network errors by returning an error from your transport rather than an HTTP response.