Testing a Go and chi RESTful API - Route Handlers (Part 1)

Testing plays a fundamental role in the development of quality software. Shipping and deploying software with undetected bugs and regressions opens up a can of terrible consequences such as losing the trust of end users or costing the business time and resources. In a large collaborative setting, having developers manually test each and every feature and user flow for bugs and regressions wastes valuable time that can be put towards improving other aspects of the software. As the codebase and team grows, this approach will not scale. By writing unit/integration/end-to-end tests, identifying and catching bugs and regressions throughout an entire codebase becomes a painless, automatable task that can easily be integrated into any continuous integration pipeline.

Unlike most other languages, the Go programming language provides a built-in, standard library package for testing: testing. The testing package offers many utilities for automating the testing of Go source files. To write a test in Go, define a function with a name prefixed with Test (followed by a capitalized segment of text) and accepts an argument of struct type T, which contains methods for failing and skipping tests, running multiple tests in parallel, formatting test logs, etc.

Example:

This test checks whether the Sum function correctly calculates the sum of two integer numbers. If the sum does not match the expected value, then the test logs an error message and marks itself as having failed.

Try it out in the Go Playground here.

Below, I'm going to show you:

  • How to write a test for a route handler of a Go and chi RESTful API.

  • How to mock a function called within a route handler.

Installation and Setup#

Clone a copy of the Go and chi RESTful API from GitHub to your machine:

This RESTful API specifies five endpoints for performing operations on posts:

  • GET /posts - Retrieve a list of posts.

  • POST /posts - Creates a post.

  • GET /posts/{id} - Retrieve a single post identified by its id.

  • PUT /posts/{id} - Update a single post identified by its id.

  • DELETE /posts/{id} - Delete a single post identified by its id.

If you would like to learn how to build this RESTful API, then please visit this blog post. In the testless branch's version of the RESTful API, the posts.go file has been moved to a routes subdirectory, and the route handlers within this file have been refactored into a routes package to allow the testing of packages independent of the main package.

Run the following command to install the project's dependencies:

Note: If you run into installation issues, then verify that the version of Go running on your machine is v1.16.

Writing a Unit Test for a Route Handler#

To get started, let's create a posts_test.go file within the routes subdirectory. This file will contain tests for each of the route handlers within the posts.go file.

Inside of posts.go, each endpoint is defined on the chi router via a corresponding routing method named as an HTTP method. To register the GET /posts endpoint on this router, call the .Get method with the route / (in main.go, this posts sub-router is attached to the main router along the /posts route).

(routes/posts.go)

rs.List (rs refers to the PostsResource{} struct) is a route handler method defined on the PostsResource{} struct. It retrieves a list of posts from the JSONPlaceholder API:

(routes/posts.go)

Let's write a unit test, which tests a single unit of code (commonly a function), for the PostsResource{}.List route handler function. Start off with a simple failing test that will immediately fail and print the message "Not yet implemented." to the terminal:

(routes/posts_test.go)

To test all packages within the current working directory recursively...

  • -v - Log verbose messages about the tests, such as the amount of time a test took to pass/fail. Prints all Log/Logf calls (automatically called by testing methods Fatal, Fatalf, etc.).

  • ./.. - Recursively tests all packages within the current working directory. Omitting this raises the error "no test files" and run none of the sub-package tests since by default, Go only tests the root package (main).

Simple Failing Test

To test the route handler for GET /posts:

  1. Create a new GET request to send to /posts via the http package's NewRequest method. This request will be passed to the route handler.

  1. The original route handler PostsResource{}.List provides a ResponseWriter, which constructs an HTTP response, and a Request, which represents an incoming HTTP request. For tests to observe and check the changes made to an HTTP response (e.g., the contents of the response body, the headers set to the response and the status code of the response), use the httptest package's NewRecorder method, which records the ResponseWriter's mutations. The ResponseRecorder is an implementation of ResponseWriter, which means any method that accepts ResponseWriter as an argument can also accept ResponseRecorder.

  1. In the context of this test, the PostsResource{}.List method is an ordinary function. To have it treated as an HTTP route handler, pass it to the http package's HandlerFunc method. Since PostsResource{}.List already has the appropriate function signature of ResponseWriter and Request, it will be called anytime its handler representation is called.

  1. Call the handler representation's ServeHTTP method to call the handler itself with the response recorder (rr) and created request (req). All of the response's data is written directly to this recorder, which substitutes the ResponseWriter. In this case, the list of posts retrieved from the JSONPlaceholder API (resp.Body in PostsResource{}.List) is copied to this recorder.

  1. If any errors were encountered while retrieving the list of posts (i.e., the JSONPlaceholder API experiencing server downtime), then the response will return a non-200 status code. When this happens, fail the test and log the error message.

  1. Decode the body of the response and store the result within a variable. If any errors are encountered while decoding the body (i.e., improperly formatted data), then fail the test and log the error message.

  1. The JSONPlaceholder API returns one hundred posts for the endpoint GET https://jsonplaceholder.typicode.com/posts. Therefore, we should expect the total items in the response body to be one hundred. Compare this expected total with the actual total from the response body. If the two values match, then pass the test. Otherwise, fail the test and log the error message.

Let's combine these code snippets together:

(routes/posts_test.go)

Run the test:

Running `PostsResource{}.List` Test

Congrats! The route handler test passes!

Mocking a Function Called Within a Route Handler#

Notice how the TestGetPostsHandler takes approximately half a second to run due to the network request the PostsResource{}.List route handler sent to the JSONPlaceholder API. If the JSONPlaceholder API experiences heavy traffic or any server outages/downtime, then the network request may either take too long to send back a response or completely time out. Because our tests rely on the status of a third-party service, which we have no control over, our tests take much longer to finish running. Let's work on eliminating this unreliability factor from TestGetPostsHandler.

PostsResource{}.List calls the GetPosts function, which sends this network request and pipes the response back into PostsResource{}.List if there was no network request error encountered.

(routes/posts.go)

Since the function responsible for sending the network request (GetPosts) is a package-scoped variable within the routes package, this function can be replaced with a mock function, which replaces the actual implementation of a function with one that simulates its behavior. Particularly, the network request will be simulated. As long as the mock function has the same function signature as the original function, calling the route handler in a test will remain the same.

Inside of posts_test.go, add a mock function for GetPosts:

(posts_test.go)

This mock function creates some dummy data (mockedPosts, which is a list containing one post), encodes this data as JSON via json.Marshal and returns a minimal HTTP response with a status code and body. These fields adhere to the http package's Response struct.

At the top of the TestGetPostsHandler test, set the GetPosts package-scoped variable to this mock function and change the expectedTotal to 1:

Run the test:

Testing with a Mocked Function

Wow! The mock allows our test to run much faster.

Next Steps#

Click here for a final version of the route handler unit test.

Try writing tests for the other route handlers.

Sources#