Hone logo
Hone
Problems

Crafting Robust Test Helpers in Go

Writing effective unit tests is crucial for maintaining code quality. Test helpers are functions and types designed to simplify and streamline the process of writing tests, reducing boilerplate and improving readability. This challenge focuses on creating a suite of reusable test helpers in Go to aid in testing HTTP handlers.

Problem Description

You are tasked with creating a package named testhelpers that provides several utility functions to simplify testing HTTP handlers in Go. The package should include functions for:

  1. CreateTestServer(handler http.Handler): This function should start a simple HTTP server using net/http/http.NewServer() (or a similar approach) and return the server's address (a string). The handler passed in should be registered as the default handler for all routes. The server should be stopped after the test completes (though you don't need to implement the stopping mechanism directly, just return the address so the test can use it).

  2. MakeRequest(t *testing.T, baseURL string, method, path string, body io.Reader, headers map[string]string) (*http.Response, error): This function should make an HTTP request to the specified baseURL using the given method, path, body, and headers. It should handle errors during the request and return the http.Response and any error encountered. The t *testing.T argument is for reporting errors within the test.

  3. AssertResponseStatus(t *testing.T, response *http.Response, expectedStatusCode int): This function should assert that the response's status code matches the expectedStatusCode. If they don't match, it should report an error using t.Errorf.

  4. ReadResponseBody(response *http.Response) (string, error): This function should read the entire body of the response as a string and return it, along with any error encountered during reading.

Examples

Example 1:

Input: handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, World!"))
})
baseURL := "http://localhost:8080"
t *testing.T
method := "GET"
path := "/"
body := nil
headers := map[string]string{}

Output: serverAddress := "http://localhost:8080"
response, err := MakeRequest(t, serverAddress, method, path, body, headers)
AssertResponseStatus(t, response, http.StatusOK)
bodyString, err := ReadResponseBody(response)
// Assert bodyString == "Hello, World!"

Explanation: The test server is started, a GET request is made to the root path, the response status is asserted to be 200, and the response body is read and can be further asserted.

Example 2:

Input: handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusBadRequest)
    w.Write([]byte("Bad Request"))
})
baseURL := "http://localhost:8080"
t *testing.T
method := "POST"
path := "/data"
body := bytes.NewBufferString("some data")
headers := map[string]string{"Content-Type": "application/json"}

Output: response, err := MakeRequest(t, baseURL, method, path, body, headers)
AssertResponseStatus(t, response, http.StatusBadRequest)
bodyString, err := ReadResponseBody(response)
// Assert bodyString == "Bad Request"

Explanation: A POST request is made with a body and headers. The response status is asserted to be 400, and the response body is read.

Example 3: (Edge Case - Request Failure)

Input: baseURL := "http://invalid-domain.com"
t *testing.T
method := "GET"
path := "/"
body := nil
headers := map[string]string{}

Output: response, err := MakeRequest(t, baseURL, method, path, body, headers)
// Expect err != nil (due to DNS resolution failure)

Explanation: The test attempts to make a request to an invalid domain. The MakeRequest function should return an error, which the test can then assert.

Constraints

  • The CreateTestServer function should return a string representing the server's address (e.g., "http://localhost:8080").
  • The MakeRequest function should use the net/http package for making requests.
  • All functions should handle errors gracefully and return them when appropriate.
  • The AssertResponseStatus function should use t.Errorf to report assertion failures.
  • The ReadResponseBody function should read the entire response body into a string.
  • The test helpers should be designed to be reusable across different tests.
  • The server created by CreateTestServer should use a port that is unlikely to be in use (e.g., a random port). You don't need to implement the port allocation, just use a reasonable default.

Notes

  • Consider using testing.T for error reporting in your helper functions.
  • Think about how to handle different HTTP methods (GET, POST, PUT, DELETE, etc.).
  • The headers parameter in MakeRequest should be a map[string]string.
  • Focus on creating clean, readable, and well-documented code.
  • You don't need to implement complex features like request timeouts or connection pooling. The goal is to create a basic set of helpers for common testing scenarios.
  • The CreateTestServer function does not need to explicitly shut down the server. The test environment will handle that. Its responsibility is to return the address.
Loading editor...
go