Testable Go HTTP Services With Graceful Shutdown
Inspired by How I write Go HTTP services after seven years, I thought it worth while to share how I’ve been writing Go HTTP services since the approach is very similar albeit with a couple of minor but useful additions.
In my use case, there’s multiple asynchronous things happening in the HTTP server using channels. As requests come in, data is being created and sent onto a channel, which is then processed in the background via go routines. The goal was 1) to create an HTTP server object that is fully testable in an end-to-end scenario. And 2) Supports graceful shutdown, such that any queues can be cleared and connections closed when the program exits.
Below is how I achieved it (minus the application specific channel logic mentioned above).
The HTTP Server Struct
First, let’s look at the HTTP server and it’s methods to start:
- NewServer(port int) simply returns a new instance. This is where you could initialize database connections before starting the HTTP server for example.
- Start() just starts the HTTP server. This method is blocking.
- Shutdown() closes the HTTP server - this is where you could also close database connections or other cleanup tasks before the program exits.
1 | package xserver |
Graceful Shutdown
The idea is to start the HTTP server, then gurantee somehow that Shutdown() is called before the program exits. There’s multiple ways to achieve this with Go. Here is the most elegant way I’ve found. I stumbled on this one while Googleing the topic one day and have used it since.
1 | package main |
The first 2 lines inside of main() initialize a channel and signal.Notify() to trigger a message being sent to the channel once an os.Interrupt is received. On line 16, the server is started in a new Go routine. Line 18 will block until it recieves the os.Interrupt signal. At that point, server.Shutdown() is called, which will gracefully close down our HTTP server. That’s all there is to it. Below is the output.
1 | go run exec/main.go |
Safe End to End Testing
The goal is the be able to start the server, use an HTTP client to send real live requests and receive responses from an instance of the HTTP server. All in a simple go test. As a first step, let’s try this (hint: this isn’t safe!)
1 | func TestServer(t *testing.T) { |
The problem with this code is that we are starting the server in the background (via go routine) which means any code immediately afterwards could be executed before or during the go routine’s execution. If you’re writing to or changing any data that depends on the server instance’s HTTP server, you can get a nil pointer error. In fact, if you take the code that initializes the router and HTTP server and move it into the Start() method, you’re guranteed to get a nil pointer error.
In order to make this safe, we need to add a method to gurantee the HTTP server is fully functional before executing tests.
1 | // ServerReady Blocks until the server returns a 200 status code from the /ping endpoint |
And then, update our tests to use it:
1 | func TestServer(t *testing.T) { |
Now our test is safe. ServerReady() simply blocks until the /ping endpoint returns a successful response back (200 status code).