Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override Go Method in Tests

So I have this Client struct that has a method UserByID that makes a HTTP request to an endpoint for a User. I want to unit test this function but also not make an actual HTTP request in the function c.Request. I want to stub that function with a response and error I can control.

func (c Client) UserByID(id string) (u User, err error) {
  v := url.Values{}
  v.Set("id", id)
  opts := Request{
    HTTP: http.Request{
        Method: http.MethodGet,
        Form:   v,
    },
    URL: 'some/endpoint/users',
  }
  resp, err := c.Request(opts)
  err = json.Unmarshal(resp, &u)
  return
}

Here's what the stub looks like:

type mockClient struct {
  Client
  fakeUser  User
  fakeError error
}

func (mc mockClient) Request(opts Request) (resp []byte, err error) {
  resp, err = json.Marshal(mc.fakeUser)
  err = mc.fakeError
  return
}

In a single test I have something like:

client := mockClient{
  fakeUser: User{},
  fakeError: nil,
}
user, err := client.UserByID(c.id)

Then I can assert the return values from client.UserByID. In this example I'm trying to override the client.Request function but I understand Go is not an inheritance-type of language. In my tests, my mockClient.Request function is not being called. The original client.Request is still being called.

I then assume that my approach is not right then. How can I test client.UserByID without actually calling the real client.Request function within it? Should the design of my methods be different?

like image 263
KA01 Avatar asked May 26 '18 16:05

KA01


People also ask

How to override a method in a class in Java?

So let us discuss how we can achieve method overriding in Java: Now create a testng.xml file for the child class and run that. If you take a look at the output, then you will find out that it will execute all the methods of child class with the overridden method.

How TestNG prioritizes and executes test cases?

If you take a look at the output, then you will find out that it will execute all the methods of child class with the overridden method. In our earlier discussion, we have seen that TestNG prioritizes and executes the test cases if we run separately both super and subclasses.

How to override configuration values in IDisposable tests?

This means that we can easily override configuration values by setting environment variables. An important gotcha is that you have to implement IDisposable, and clear the environment variable after the test ran, otherwise it could affect subsequent tests.

How to execute all the methods of a child class in TestNG?

Now create a testng.xml file for the child class and run that. If you take a look at the output, then you will find out that it will execute all the methods of child class with the overridden method. In our earlier discussion, we have seen that TestNG prioritizes and executes the test cases if we run separately both super and subclasses.


2 Answers

To accomplish what you need, you can re-structure your code just a little bit.

You can find a full working example here: https://play.golang.org/p/VoO4M4U0YcA

And below is the explanation.

First, declare a variable function in your package to encapsulate the actual making of the HTTP request:

var MakeRequest = func(opts Request) (resp []byte, err error) {
    // make the request, return response and error, etc
}

Then, in your Client use that function to make the request:

func (c Client) Request(opts Request) (resp []byte, err error) {
    return MakeRequest(opts)
}

In that way, when you actually use the client, it will make the HTTP request as expected.

But then when you need to test, you can assign a mock function to that MakeRequest function so that you can control its behaviour:

// define a mock requester for your test

type mockRequester struct {
    fakeUser  User
    fakeError error
}

func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
    resp, err = json.Marshal(mc.fakeUser)
    err = mc.fakeError
    return
}

// to use it, you can just point `MakeRequest` to the mock object function

mockRequester := mockRequester{
    fakeUser:  User{ ID: "fake" },
    fakeError: nil,
}
MakeRequest = mockRequester.Request
like image 50
eugenioy Avatar answered Nov 15 '22 18:11

eugenioy


I then assume that my approach is not right then.

Your description covers it Exactly! Even though you're embedding the Client in mockClient when you call client.UserByID(c.id) go looks at the mockClient and sees the method pulled up from Client . it ends up so that the Client!!! is the receiver to UserByID call NOT the mockClient. You can see this here:

func (c Client) UserByID(id string) (u User, err error)

Once the Client is the receiver resp, err := c.Request(opts) is called with the Client receiver above and NOT your mockClient as you're observing.


One way to introduce a seam for c.Request that you can provide a custom implementation for use in unit testing is to make Request a callout method on your Client struct.

type Client struct {
    Request func(opts Request) (resp []byte, err error) 
}

The above should help to decouple Client from Request implementation. All it says is that Request will be a function that takes some args with some return value, allowing you to substitute different functions depending if you're in production or testing. Now during your public initialization of Client you can provide your real implementation of Request, while in unit tests you can provide your fake implementation.

type mockRequester struct {
  fakeUser  User
  fakeError error
}

func (mc mockRequester) Request(opts Request) (resp []byte, err error) {
  resp, err = json.Marshal(mc.fakeUser)
  err = mc.fakeError
  return
}

mr := mockRequester{...}
c := Client{  
  Request: mr.Request,
}

This comes with its own tradeoffs though as you potentially lose the client as a pointer receiver in your Request callout function.

Another cool part of the Callout is that it gives you another option of encapsulation. Suppose in the future you'd like to provide some sort of exponential backoff or retry. It would allow you to provide a more intelligent Request method to Client without Client having to change.

like image 28
dm03514 Avatar answered Nov 15 '22 17:11

dm03514