Testing a Go and chi RESTful API - Route Handlers and Middleware (Part 2)
Disclaimer - If you are unfamiliar with writing a simple unit test for a route handler of a Go and chi RESTful API, then please read this blog post before proceeding on. Go's testing library package provides many utilities for automating the testing of Go code. To write robust tests for Go code, you must already understand how to write a basic Go test suite that contains several TestXxx functions. Writing tests for code, especially within the context of test-driven development (TDD), prioritizes the code's correctness over the code's flexibility to adapt to new/updated requirements. The guarantee of less unexpected regressions offsets the upfront cost of spending more time to write tests alongside application code. In a fast-paced, high-pressure environment, it can be difficult to convince other team members and stakeholders of the value in testing code when time is an extremely limited resource. Another factor that must be considered is the amount of code covered by the tests. If the tests cover only a small percentage of the application code (or a small subset of use cases), then the benefits of having these tests probably won't outweigh the benefits of adding new features or improving existing features. Plus, anytime you decide to refactor the application code, you will also have to update the corresponding tests to reflect these changes. When time is so valuable, the time spent on writing tests could have been spent elsewhere. Therefore, to fully benefit from tests, you must write enough tests such that they cover a large percentage of the application code. If a RESTful API exposes multiple endpoints, then testing a single route handler won't bring much value for the time spent writing it. Testing RESTful APIs built with Go and chi requires testing not only all of the route handlers, but also, the chi /custom middleware handlers. Let's write tests for... Clone a copy of the Go and chi RESTful API (with a basic test for a single endpoint) from GitHub to you machine: This RESTful API specifies five endpoints for performing operations on posts: If you would like to learn how to build this RESTful API, then please visit this blog post . 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 . There is a single test within the routes/posts_test.go file: TestGetPostsHandler . It tests the route handler for the GET /posts endpoint, and it mocks out the GetPosts function called by the route handler to avoid sending a network request to the JSONPlaceholder API when executing tests. Compared to testing route handlers for GET requests, testing route handlers for other HTTP methods (e.g., POST and PUT ) involves slightly more code to test requests that carry data in the form of an optional request body: Let's write a test for the POST /posts endpoint. Before starting, we must first review the route handler for this endpoint ( Create ) in the routes/posts.go file. ( routes/posts.go ) To test the route handler Create , we must first mock out the CreatePost function to avoid sending a network request to the JSONPlaceholder API when executing tests. CreatePost accepts a request body and returns an HTTP response and error (if encountered). Therefore, the mock function must follow the same function signature as CreatePost , like so: CreatePost is defined on type JsonPlaceholderMock . The request body must contain the following information: Since this request body (JSON data) must be passed as type io.ReadCloser to CreatePost , it must be read into a buffer, converted into a byte slice and then decoded into a Go struct so the data can be accessed normally. When a POST /posts request is sent to the JSONPlaceholder API, it returns a response that contains the newly created post. This post contains the exact same information as the request body with an additional Id field. This Id field is set to 101 to imply that a new post was added since there is a total of 100 posts, and each post has an Id field set to 1 , 2 , 3 , etc., up to 100 . The JSONPlaceholder API doesn't actually create this new resource, but rather, fakes it. So anytime you send a request to POST /posts , the response returned will have a newly created post with an Id field set to 101 . In our mock function, let's create this dummy data to send back in the response. Encode this struct to JSON via the json.Marshal method and return it within the body of a HTTP response. This HTTP response should return with a 200 status code to indicate a successful request. Note : This HTTP response returned must adhere to the http.Response struct , which accepts an integer value for its StatusCode field and a value of type io.ReadCloser for its Body field. The ioutil.NopCloser method returns a ReadCloser that wraps the Reader (in this case, bytes.NewBuffer(respBody) , which prepares a buffer to read respBody ) with a no-op Close method, which allows the Reader to adhere to the ReadCloser interface. Putting it altogether... With the mock function now implemented, let's write the test for the route handler Create for POST /posts . Start by naming the unit test TestCreatePostHandler , like so... Then, write this test similar to the TestGetPostsHandler test, but with several adjustments to account for it testing a POST route handler: Putting it altogether... To test the chi logger middleware handler ( middleware.Logger ), we must first understand the implementation details of this handler function: ( go-chi/chi/middleware/logger.go ) The middleware.Logger function only calls a package-scoped function, DefaultLogger , which logs information for each incoming request. ( go-chi/chi/middleware/logger.go ) This package-scoped function accepts a handler function and returns a handler function. Therefore, we can write a test that involves passing a simple route handler to middleware.DefaultLogger and calling middleware.DefaultLogger 's ServeHTTP method with a response recorder and created request: Try adding more endpoints and writing tests for those endpoints' route handlers.