What's the most idiomatic way of testing functions with HTTP requests in Go?

842 views Asked by At

I have a fun little weather app. For only $99/day, the app will check the weather daily, and if it's raining in Seattle, send an umbrella to the people of San Diego.

I use these two functions as part of my app:

func IsRaining() (bool, error) {
    resp, err := http.Get("https://isitraining.in/Seattle")
    if err != nil {
        return false, fmt.Errorf("could not fetch raining status: %w", err)
    }

    parsed, err := weather.Parse(resp)
    if err != nil {
        return false, fmt.Errorf("could not parse the weather: %w", err)
    }

    return parsed.IsRaining, nil
}

func SendUmbrella() error {
    postData := umbrellaPostData()
    resp, err := http.Post("https://amazon.com", "text/html", &postData)
    if err != nil {
        return fmt.Errorf("could not send umbrella: %w", err)
    }
    return nil
}

I want to test IsRaining() and SendUmbrella(), but I don't want to have to actually send someone an umbrella every time I run my tests; my engineers use TDD and I do have a budget, you know. Same thing with IsRaining(), what if the internet is down? I still need to be able to run by tests, rain or shine.

I want to do this in such a way that the code stays ergonomic and readable, but I definitely need to be able to test those HTTP-dependent functions. What's the most idiomatic way to do this in Go?

P.S. I'm using Testify. Tell me all about how I just lost any hope of idiomatic Go in the comments :)

1

There are 1 answers

0
Schwern On

I don't know about "most idiomatic", but same as in any other language hard coded classes packages are a headache. Instead of calling methods directly on the http package, make an httpClient interface. Then mock the httpClient interface.

You could pass the httpClient into the function, but it makes more sense to turn these into methods on a struct.

// Set up an interface for your http client, same as http.Client.
type httpClient interface {
    Get(string) (*http.Response, error)
}

// Make a struct to hang the client and methods off of.
type umbrellaGiver struct {
    client httpClient
}

// A cut down example method.
func (giver umbrellaGiver) getExample() ([]byte, error) {
    resp, err := giver.client.Get("https://example.com")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

Then a mocked httpClient can be put into your umbrellaGiver.

// Our mocked client.
type mockedClient struct {
    mock.Mock
}

// Define the basic mocked Get method to record its arguments and
// return its mocked values.
func (m mockedClient) Get(url string) (*http.Response, error) {
    args := m.Called(url)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    } else {
        return args.Get(0).(*http.Response), args.Error(1)
    }
}

func main() {
    // Make a mockedClient and set up an expectation.
    client := new(mockedClient)

    // Make an umbrellaGiver which uses the mocked client.
    s := umbrellaGiver { client: client }

    // Let's test what happens when the call fails.
    client.On(
        "Get", "https://example.com",
    ).Return(
        nil, errors.New("The system is down"),
    )

    body, err := s.getExample()
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s", body)
}

See Mocking HTTP Requests in Golang