Search code examples
unit-testinggotesting

Golang mock forces to change functions definition


I have the following function:

func getPrice(date string) {
  url := (fmt.Printf("http://endponint/%s", date))
    resp, err := http.Get(url)
    // Unmarshall body and get price
    return price
}

In order to unit test this function I'm forced to refactor to:

func getPrice(client HTTPClient, date string) {
  url := (fmt.Printf("http://endponint/%s", date))
    resp, err := client.Get(url)
    // Unmarshall body and get price
    return price
}

And my test looks like this:


type MockClient struct {
    Response *http.Response
    Err      error
}

func (m *MockClient) Get(url string) (*http.Response, error) {
    return m.Response, m.Err
}

mockResp := &http.Response{
    StatusCode: http.StatusOK,
    Body:       ioutil.NopCloser(strings.NewReader("mock response")),
}

client := &MockClient{
    Response: mockResp,
    Err:      nil,
}

data, err := getData(client, "http://example.com")

Is this only way to test in Go, is there no way to mock an API which is not injected into function?


Solution

  • The idiomatic way to do HTTP testing with Go is using http/httptest (examples)

    In your case, all you would need to do is make the base URL injectable:

    var APIEndpoint = "http://endpoint/"
    
    func getPrice(date string) (int, error) {
      url := (fmt.Printf("%s/%s", APIEndpoint, date))
      resp, err := http.Get(url)
      // Unmarshall body and get price
      return price, nil
    }
    

    And then in each test:

    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Write the expected response to the writer
    }))
    defer srv.Close()
    APIEndpoint = srv.URL
    price, err := getPrice("1/1/2023")
    // Handle error, check return
    

    A slightly better design would be to wrap your API in a client struct and make getPrice a receiver method:

    type PriceClient struct {
        Endpoint string
    }
    
    func (pc *PriceClient) GetPrice(date string) (int, error) {
      url := (fmt.Printf("%s/%s", pc.Endpoint, date))
      resp, err := http.Get(url)
      // Unmarshall body and get price
      return price, nil
    }
    
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Write the expected response to the writer
    }))
    defer srv.Close()
    c := &PriceClient{Endpoint: srv.URL}
    price, err := c.GetPrice("1/1/2023")
    // Handle error, check return
    

    For future reference, you should also have a look at gomock, since most other mocking problems you'll encounter do not have solutions built-in to the language.