How to Test Go HTTP Handlers

April 29, 2024
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Test Go HTTP Handlers

Testing is essential in any development process — especially when working with web applications. In Go, HTTP handlers are used to handle incoming requests and send responses back to clients. To ensure that your HTTP handlers are functioning properly, you need to test them thoroughly.

In this tutorial, you'll learn the essentials of how to test HTTP handlers in Go using the httptest package.

Prerequisites

Before we begin, ensure that you have the following:

  • An understanding of or prior experience with Go
  • Go version 1.22 or higher

What are HTTP handlers?

HTTP handlers are simply functions that handle incoming HTTP requests and send responses back to the client. Implementing the Handler interface, they take an http.ResponseWriter object and a pointer to an http.Request object, and write to the response, such as by setting the body of the response or redirecting the user within or outside the application.

HTTP handlers are typically registered using the http.HandleFunc() function, which maps a URL pattern to a handler function. When an HTTP request comes in the net/http package matches the request against the registered URL patterns and calls the corresponding handler function if a match is found.

Why do we test HTTP Handlers?

Here are three good reasons why we test HTTP handlers:

  • Improved Reliability: Tests help identify bugs and potential issues in your handlers before they reach production. This reduces the risk of unexpected errors and outages in your web application.
  • Increased Confidence: By having well-tested handlers, you gain confidence that your application will behave as intended under various conditions. This allows for faster development cycles and more reliable deployments.
  • Easier Maintenance: Tests serve as documentation for your code, explaining how handlers are expected to work with different inputs. This makes it easier to understand existing code and maintain it in the future.

How to test HTTP handlers using httptest

httptest is a standard Go library package that provides us with a set of tools for testing HTTP servers and clients. It provides functionality to mock requests to your HTTP handlers, eliminating the need to run a real server during testing. This is particularly useful for unit testing, where you want to test the behavior of your handlers in isolation.

Here’s a breakdown of how it works. The first step in testing HTTP handlers is to create mock HTTP requests. To do this, you use the http.NewRequest() function, which allows you to simulate different types of HTTP request methods (GET, POST, PUT, DELETE, etc.) with various query parameters, headers, and body content. This sets the conditions under which your handler will be tested.

For example, to create a GET request with query parameters and a header you could use the following code:

req, err := http.NewRequest("GET", "/api/projects", url.Values{"page": {"1"}, "per_page": {"100"}})
if err != nil {
    t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer abc123")

Next, you create a ResponseRecorder object using httptest.NewRecorder(). This is an implementation of http.ResponseWriter that records the expected function calls for later inspection in tests. You pass this ResponseRecorder to your handler function along with the mock request. The handler then processes the request and writes the response to the ResponseRecorder.

rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)

The ResponseRecorder allows you to capture the response generated by your handler After the handler has processed the request and written the response to the ResponseRecorder, you can inspect the recorded response to verify that it matches your expectations. This includes checking the status code, headers, and body of the response. For example:

if status := rr.Code; status != http.StatusOK {
    t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}

expected := "Expected response body"
if rr.Body.String() != expected {
    t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}

An example of how to test HTTP handlers in Go using httptest

Imagine a scenario where we have an HTTP handler designed to process POST requests containing JSON data and respond with JSON data. This handler checks for a valid POST request, ensuring the request method is correct, decodes the JSON body into a struct, and validates the presence of required fields. If any of these checks fail, it returns an appropriate HTTP status code and error message. We can use the httptest package to test this handler's for these scenarios ensuring the handler behaves as expected under different conditions.

To get started, two files will be created: one for the handler and another for the handler's tests.

Create the handler file

Create a directory for this project and add module support by running the command below.

mkdir test-http-handlers
cd test-http-handlers
go mod init test-http-handlers

Then, inside the new directory, create a file called handler.go. In the new file, add the following code:

package handler

import (
   "encoding/json"
   "net/http"
)

type Data struct {
   Name  string `json:"name"`
   Email string `json:"email"`
}

func ProcessDataHandler(w http.ResponseWriter, r *http.Request) {
   if r.Method != http.MethodPost {
       http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
       return
   }

   var d Data
   err := json.NewDecoder(r.Body).Decode(&d)
   if err != nil {
       http.Error(w, "Invalid JSON data", http.StatusBadRequest)
       return
   }

   if d.Name == "" || d.Email == "" {
       http.Error(w, "Missing data", http.StatusBadRequest)
       return
   }

   response := map[string]string{
       "status":  "success",
       "message": "Data processed successfully",
   }

   w.Header().Set("Content-Type", "application/json")
   json.NewEncoder(w).Encode(response)
}

The processDataHandler() function checks the request method, attempts to decode the JSON body into a Data struct, and validates the presence of the Name and Email fields. If any of these checks fail, it returns an appropriate HTTP status code and error message.

Create the test file

Inside the same directory as handlers.go, create a file called handler_test.go. The httptest.NewRequest() function will be utilized to create a mock POST request with JSON data, and httptest.NewRecorder() to capture the response.

Inside the file, add the following code:

package handler

import (
   "bytes"
   "encoding/json"
   "net/http"
   "net/http/httptest"
   "testing"
)

func TestProcessDataHandler(t *testing.T) {
   validData := []byte(`{"name": "John Doe", "email": "john.doe@example.com"}`)
   req := httptest.NewRequest(http.MethodPost, "/process-data", bytes.NewBuffer(validData))
   req.Header.Set("Content-Type", "application/json")
   w := httptest.NewRecorder()
   ProcessDataHandler(w, req)
   if w.Code != http.StatusOK {
       t.Errorf("Expected status code %v, got %v", http.StatusOK, w.Code)
   }
   var response map[string]string
   json.Unmarshal(w.Body.Bytes(), &response)
   if response["status"] != "success" || response["message"] != "Data processed successfully" {
       t.Errorf("Unexpected response body: %v", response)
   }

   req = httptest.NewRequest(http.MethodGet, "/process-data", nil)
   w = httptest.NewRecorder()
   ProcessDataHandler(w, req)
   if w.Code != http.StatusMethodNotAllowed {
       t.Errorf("Expected status code %v, got %v", http.StatusMethodNotAllowed, w.Code)
   }

   invalidJSON := []byte(`{"name": "John Doe", "email": "john.doe@example.com"`) // Missing closing brace
   req = httptest.NewRequest(http.MethodPost, "/process-data", bytes.NewBuffer(invalidJSON))
   req.Header.Set("Content-Type", "application/json")
   w = httptest.NewRecorder()
   ProcessDataHandler(w, req)
   if w.Code != http.StatusBadRequest {
       t.Errorf("Expected status code %v, got %v", http.StatusBadRequest, w.Code)
   }

   missingData := []byte(`{"name": "John Doe"}`)
   req = httptest.NewRequest(http.MethodPost, "/process-data", bytes.NewBuffer(missingData))
   req.Header.Set("Content-Type", "application/json")
   w = httptest.NewRecorder()
   ProcessDataHandler(w, req)
   if w.Code != http.StatusBadRequest {
       t.Errorf("Expected status code %v, got %v", http.StatusBadRequest, w.Code)
   }
}

In the code above, the TestProcessDataHandler function utilizes the httptest package to test for various request scenarios. These include valid POST requests, invalid request methods, invalid JSON data, and missing data in the JSON payload. For each scenario, it creates a mock request and response writer, then calls the ProcessDataHandler function with these mock objects. This approach allows for the testing of how the handler responds to different types of requests and data inputs, ensuring that the handler behaves correctly under various conditions.

The test also checks the HTTP status code returned by the handler and the body of the response to ensure they match the expected values for each scenario. This ensures the reliability and robustness of the HTTP handler, as it verifies that the handler can handle a wide range of inputs and conditions effectively.

To run the test, run this command in your terminal:

go test -v

You should have something like this shown in the terminal after running the test:

Result in the terminal showing the tested handler function passes.

Populate a context for HTTP handler tests

When testing HTTP handlers that expect data to be passed through a context.Context, creating and populating a context with the necessary data for tests is essential. The data can include authentication tokens, user information, or any other data that the handler might need to perform its tasks. The context.WithValue() function is used to add values to the context, which can then be retrieved in the handler using the context.Value() function.

To illustrate this, we'll create an example demonstrating how to use context values in an HTTP handler and test such a handler.We'll create a simple HTTP handler that retrieves a value from the context and uses it to respond to an HTTP request.

Create the handler file

Now, we'll create an HTTP handler function that retrieves a value from a request context. In handler.go, replace the existing code with the following:

package handler

import (
    "context"
    "net/http"
)

func GetValue(ctx context.Context, key interface{}) (interface{}, bool) {
    value := ctx.Value(key)
    if value == nil {
        return nil, false
    }
    return value, true
}

func DataHandler(w http.ResponseWriter, r *http.Request) {
    value, ok := GetValue(r.Context(), "example_key")
    if !ok {
        http.Error(w, "Value not found", http.StatusNotFound)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(value.(string)))
}

In the code above, the Datahandler() function uses the GetValue() function to retrieve a value from the request context. It demonstrates how to use context values in an HTTP handler to access data that has been passed through the context.

Create the test file

Now, inside the handler_test.go, replace the existing code with the following:

package handler

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"

    "your_project_path/handler" // Replace with your actual project path
)

func TestDataHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/example", nil)
    if err != nil {
        t.Fatal(err)
    }
    ctx := context.WithValue(req.Context(), "example_key", "example_value")
    req = req.WithContext(ctx)

    rr := httptest.NewRecorder()

    DataHandler(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    expectedBody := "example_value"
    if rr.Body.String() != expectedBody {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expectedBody)
    }
}

The TestDataHandle() function creates a mock HTTP request and populates its context with a value using context.WithValue.

Check the result in the terminal using the command below:

go test -v

You should see output similar to the following:

Terminal output displaying the successful passing of the test for the HTTP handler, verifying its ability to retrieve a value from the context and respond to an HTTP request.

Mock database calls

Mocking database calls is crucial for testing HTTP handlers that interact with a database. It allows you to simulate database interactions without needing an actual database, making your tests faster, more reliable, and easier to set up and tear down. Let's create a simple example to illustrate this concept.

Create the handler file

We’ll define a handler package that includes a User struct, a UserDB interface, a MockUserDB struct that implements the UserDB interface, and a GetUserHandler() function. Replace the code in handler.go with the following.

package handler

import (
   "encoding/json"
   "errors"
   "net/http"
   "strconv"
)

type User struct {
   ID        int
   Email     string
   FirstName string
   LastName  string
}

type UserDB interface {
   GetUserByID(id int) (*User, error)
}

type MockUserDB struct {
   User *User
   Err  error
}

func (m *MockUserDB) GetUserByID(id int) (*User, error) {
   if m.Err != nil {
       return nil, m.Err
   }

   if m.User == nil {
       return nil, errors.New("user not found")
   }

   return m.User, nil
}

func GetUserHandler(db UserDB) http.HandlerFunc {
   return func(w http.ResponseWriter, r *http.Request) {
       idStr := r.URL.Query().Get("id")
       id, err := strconv.Atoi(idStr)
       if err != nil {
           http.Error(w, "Invalid ID", http.StatusBadRequest)
           return
       }

       user, err := db.GetUserByID(id)
       if err != nil {
           http.Error(w, "User not found", http.StatusNotFound)
           return
       }

       json.NewEncoder(w).Encode(user)
   }
}

Create the test file

The next thing is to test the GetUserHandler() function. This package uses MockUserDB to simulate different database behaviors. Inside the test_handers.go, replace the code with the following:

package handler

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestGetUserHandler(t *testing.T) {
	t.Run("successful retrieval", func(t *testing.T) {
		mockDB := &MockUserDB{
			User: &User{ID: 1, Email: "test@example.com", FirstName: "John", LastName: "Doe"},
		}
		handler := GetUserHandler(mockDB)
		req, _ := http.NewRequest("GET", "/user?id=1", nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if status := rr.Code; status != http.StatusOK {
			t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
		}
	})

	t.Run("user not found", func(t *testing.T) {
		mockDB := &MockUserDB{
			Err: errors.New("user not found"),
		}
		handler := GetUserHandler(mockDB)
		req, _ := http.NewRequest("GET", "/user?id=1", nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if status := rr.Code; status != http.StatusNotFound {
			t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
		}
	})

	t.Run("invalid ID", func(t *testing.T) {
		mockDB := &MockUserDB{
			User: &User{ID: 1, Email: "test@example.com", FirstName: "John", LastName: "Doe"},
		}
		handler := GetUserHandler(mockDB)
		req, _ := http.NewRequest("GET", "/user?id=invalid", nil)
		rr := httptest.NewRecorder()
		handler.ServeHTTP(rr, req)
		if status := rr.Code; status != http.StatusBadRequest {
			t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
		}
	})
}

After setting up the handler and handler_test packages as described, you can run the tests using the go test command:

go test -v

The output should look similar to the screenshot below:

erminal output demonstrating the test results for the defined handler package, including the User struct, UserDB interface, MockUserDB struct implementing UserDB, and the GetUserHandler function.

Using a mock database, you can simulate various scenarios, such as successful data retrieval, user not found errors, and invalid IDs, making your tests more reliable and faster to execute.

That's how to test HTTP handlers in Go

In this article, we looked at how to test HTTP handlers in Go. Testing HTTP handlers in Go is crucial for ensuring the reliability and correctness of web applications. By following the methods outlined in this article, you can effectively test your HTTP handlers, ensuring they function as expected and handle requests appropriately.

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.