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: 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: 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 . 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... To test the route handler for GET /posts : Let's combine these code snippets together: ( routes/posts_test.go ) Run the test: Congrats! The route handler test passes! 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: Wow! The mock allows our test to run much faster. Click here for a final version of the route handler unit test. Try writing tests for the other route handlers.