2

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 :)

  • I upvoted for originality in contriving your question! – Dai Apr 27 '21 at 22:14
  • 1
    Also, to actually answer your question: by mocking your HTTP client: https://www.thegreatcodeadventure.com/mocking-http-requests-in-golang/ - the article's eventual conclusion can be summarized as: use dependency-injection and interact with services (where a "service" is something like the HTTP client type - not a web-service) only through interfaces. This might start you off on a major refactoring binge btw - especially if your application isn't already using DI. Unfortunately I don't believe Golang comes with an opinionated DI container yet... lemme check – Dai Apr 27 '21 at 22:15
  • UPDATE: Judging by this question it looks like the Go community hasn't settled on a good DI solution yet: https://stackoverflow.com/questions/41900053/is-there-a-better-dependency-injection-pattern-in-golang despite Google introducing their own DI container in 2018: https://blog.golang.org/wire – Dai Apr 27 '21 at 22:18
  • UPDATE2: Actually that Golang article from 2018 looks like an excellent resource for you and anyone else wanting to use DI in Golang. – Dai Apr 27 '21 at 22:19
  • 1
    Given your use case, couldn't isRaining always return true? – Don Branson Apr 27 '21 at 22:25
  • 2
    Using testify is the opposite of „idiomatic“ so why bother? – Volker Apr 28 '21 at 05:01

1 Answers1

1

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

Schwern
  • 153,029
  • 25
  • 195
  • 336