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?
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.
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.
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.
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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With