DEV Community

loading...

Mocking HTTP Call in Golang

clavinjune profile image Clavin June Originally published at clavinjune.dev on ・3 min read

Photo by @kellysikkema on Unsplash

This blog post code is running on go1.16.2

API Interface to be tested

type API interface {
  // this function will do http call to external resource
  FetchPostByID(ctx context.Context, id int) (*APIPost, error)
}

type APIPost struct {
  ID int `json:"id"`
  UserID int `json:"userId"`
  Title string `json:"title"`
  Body string `json:"body"`
}
Enter fullscreen mode Exit fullscreen mode

We can simply mock the API interface FetchPostByID function result in our unit test by creating a mock implementation of the API interface like this:

API Mock implementation

type APIMock struct {}

func (a APIMock) FetchPostByID(ctx context.Context, id int) (*APIPost, error) {
  return nil, fmt.Errorf(http.StatusText(http.StatusNotFound))
}
Enter fullscreen mode Exit fullscreen mode

But by doing that, it doesn’t increase the test coverage and it will skip the rest of the code inside the FetchPostByID real implementation.

So we’re gonna make the testable real implementation first of the API interface.

Implementation

To mock only the HTTP Call, we need to create http.Client mock implementation. the real http.Client have Do function that executed whenever we want to do HTTP call. So we need to mock the Do function. Because http.Client doesn’t have any interface implemented by it, we need to create one.

HTTP Client Mock

type HTTPClient interface {
  Do(*http.Request) (*http.Response, error)
}

type HTTPClientMock struct {
  // DoFunc will be executed whenever Do function is executed
  // so we'll be able to create a custom response
  DoFunc func(*http.Request) (*http.Response, error)
}

func (H HTTPClientMock) Do(r *http.Request) (*http.Response, error) {
  return H.DoFunc(r)
}
Enter fullscreen mode Exit fullscreen mode

API Implementation Struct

func NewAPI(client HTTPClient, baseURL string, timeout time.Duration) API {
  return &apiV1{
    c: client,
    baseURL: baseURL,
    timeout: timeout,
  }
}

type apiV1 struct {
  // we need to put the http.Client here
  // so we can mock it inside the unit test
  c HTTPClient
  baseURL string
  timeout time.Duration
}

func (a apiV1) FetchPostByID(ctx context.Context, id int) (*APIPost, error) {
  u := fmt.Sprintf("%s/posts/%d", a.baseURL, id)

  ctx, cancel := context.WithTimeout(ctx, a.timeout)
  defer cancel()

  req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
  if err != nil {
    return nil, err
  }

  resp, err := a.c.Do(req)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()

  if resp.StatusCode != http.StatusOK {
    return nil, fmt.Errorf(http.StatusText(resp.StatusCode))
  }

  var result *APIPost
  return result, json.NewDecoder(resp.Body).Decode(&result)
}
Enter fullscreen mode Exit fullscreen mode

Unit Test

var (
  // our custom client
  client = &HTTPClientMock{}
  // our api
  api = NewAPI(client, "", 0)
)

func TestApiV1_FetchPostByID(t *testing.T) {
  // test table
  tt := []struct {
    // Body mock the response body
    Body string
    // StatusCode mock the response statusCode
    StatusCode int

    // Expected Result
    Result *APIPost
    // Expected Error
    Error error
  }{
    {
      Body: `{"userId": 1,"id": 1,"title": "test title","body": "test body"}`,
      StatusCode: 200,
      Result: &APIPost{
        ID: 1,
        UserID: 1,
        Title: "test title",
        Body: "test body",
      },
      Error: nil,
    },
    {
      Body: `{"userId": 2,"id": 2,"title": "test title2","body": "test body2"}`,
      StatusCode: 200,
      Result: &APIPost{
        ID: 2,
        UserID: 2,
        Title: "test title2",
        Body: "test body2",
      },
      Error: nil,
    },
    {
      Body: ``,
      StatusCode: http.StatusNotFound,
      Result: nil,
      Error: fmt.Errorf(http.StatusText(http.StatusNotFound)),
    },
    {
      Body: ``,
      StatusCode: http.StatusBadRequest,
      Result: nil,
      Error: fmt.Errorf(http.StatusText(http.StatusBadRequest)),
    },
  }

  for _, test := range tt {
    // we adjust the DoFunc for each test case
    client.DoFunc = func(r *http.Request) (*http.Response, error) {
      return &http.Response{
        // create the custom body
        Body: io.NopCloser(strings.NewReader(test.Body)),
        // create the custom status code
        StatusCode: test.StatusCode,
      }, nil
    }

    // execute the func
    p, err := api.FetchPostByID(context.Background(), 0)

    // validation
    if err != nil && err.Error() != test.Error.Error() {
      t.Fatalf("want %v, got %v", test.Error, err)
    }

    if !reflect.DeepEqual(p, test.Result) {
      t.Fatalf("want %v, got %v", test.Result, p)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Because we only change the http.Client, our FetchPostByID func is tested as it is except for this line:

resp, err := a.c.Do(req)
Enter fullscreen mode Exit fullscreen mode

Because the a.c.Do is already adjusted with our mock DoFunc inside the unit test, the a.c.Do behavior will be changed according to this line:

client.DoFunc = func(r *http.Request) (*http.Response, error) {
  return &http.Response{
    Body: io.NopCloser(strings.NewReader(test.Body)),
    StatusCode: test.StatusCode,
  }, nil
}
Enter fullscreen mode Exit fullscreen mode

Let’s run the test

$ go test ./... -race -coverprofile /tmp/coverage.out && go tool cover -html=/tmp/coverage.out
Enter fullscreen mode Exit fullscreen mode

Test Coverage

Discussion (0)

pic
Editor guide